Clojure 中多方法与 cond 的性能

Performance of multimethod vs cond in Clojure

Multimethods 比协议慢,当协议可以解决问题时应该尝试使用协议,即使使用 multimethods 提供了更灵活的解决方案。 那么 cond 和 multimethod 是怎么回事呢?它们可用于解决相同的问题,但我的猜测是 multimethod 与 cond 相比具有巨大的性能开销。如果是这样,我为什么要使用多方法而不是 cond

Multimethods允许开放扩展;其他人可以通过在其源代码中添加新的 defmethod 来扩展您对任意表达式的多方法调度。在不编辑 cond 源代码的情况下,Cond 表达式无法被其他人甚至您自己的代码扩展。

如果您只想根据条件逻辑执行操作,那么 cond 是最佳选择。如果您想进行更复杂的调度,或将函数应用于具有不同行为的多种类型的数据,那么多方法可能更合适。

既然可以测量,为什么还要担心?

这是使用 criterium library. Cond and Multi-methods codes are taken from http://blog.8thlight.com/myles-megyesi/2012/04/26/polymorphism-in-clojure.html 的基准示例。

警告 这只是比较 multimethodcond 性能的基准测试示例。下面的结果表明 condmultimethod 表现更好,不能推广到实践中的各种用法。您可以将此基准测试方法用于您自己的代码。

;; cond
(defn convert-cond [data]
   (cond
     (nil? data)
       "null"
     (string? data)
       (str "\"" data "\"")
     (keyword? data)
       (convert-cond (name data))
     :else
     (str data)))


(bench (convert-cond "yolo"))

Evaluation count : 437830380 in 60 samples of 7297173 calls.

             Execution time mean : 134.822430 ns
    Execution time std-deviation : 1.134226 ns
   Execution time lower quantile : 133.066750 ns ( 2.5%)
   Execution time upper quantile : 137.077603 ns (97.5%)
                   Overhead used : 1.893383 ns

Found 2 outliers in 60 samples (3.3333 %)
    low-severe   2 (3.3333 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers

;; multimethod
(defmulti convert class)

(defmethod convert clojure.lang.Keyword [data]
  (convert (name data)))

(defmethod convert java.lang.String [data]
   (str "\"" data "\""))

(defmethod convert nil [data]
   "null")

 (defmethod convert :default [data]
   (str data))

(bench (convert "yolo"))
Evaluation count : 340091760 in 60 samples of 5668196 calls.

             Execution time mean : 174.225558 ns
    Execution time std-deviation : 1.824118 ns
   Execution time lower quantile : 170.841203 ns ( 2.5%)
   Execution time upper quantile : 177.465794 ns (97.5%)
                   Overhead used : 1.893383 ns
nil

为了跟进@AlexMiller 的评论,我尝试使用更多随机数据进行基准测试并添加了协议实现(还向不同的方法添加了另一种类型 - Integer)。

(defprotocol StrConvert
  (to-str [this]))

(extend-protocol StrConvert
  nil
  (to-str [this] "null")
  java.lang.Integer
  (to-str [this] (str this))
  java.lang.String
  (to-str [this] (str "\"" this "\""))
  clojure.lang.Keyword
  (to-str [this] (to-str (name this)))
  java.lang.Object
  (to-str [this] (str this)))

data 包含 10000 个随机整数的序列,这些整数随机转换为 Stringnilkeywordvector

(let [fns [identity            ; as is (integer)
           str                 ; stringify
           (fn [_] nil)        ; nilify
           #(-> % str keyword) ; keywordize
           vector]             ; vectorize
      data (doall (map #(let [f (rand-nth fns)] (f %))
                       (repeatedly 10000 (partial rand-int 1000000))))]
  ;; print a summary of what we have in data
  (println (map (fn [[k v]] [k (count v)]) (group-by class data)))
  ;; multimethods
  (c/quick-bench (dorun (map convert data)))
  ;; cond-itionnal
  (c/quick-bench (dorun (map convert-cond data)))
  ;; protocols
  (c/quick-bench (dorun (map to-str data))))

data 的结果包含:

([clojure.lang.PersistentVector 1999] [clojure.lang.Keyword 1949]
 [java.lang.Integer 2021] [java.lang.String 2069] [nil 1962])
  • 多方法:6.26 毫秒
  • 条件:5.18 毫秒
  • 协议:6.04 毫秒

我肯定会像@DanielCompton 那样建议:至少在这个例子中,设计比每种方法成对出现的纯性能更重要。