Clojure中多方法vs cond的性能

Performance of multimethod vs cond in Clojure

多方法比协议慢,即使使用多方法可以提供更灵活的解决方案,也应该在协议可以解决问题时尝试使用协议。
那么cond和multimethod是什么情况?它们可以用来解决相同的问题,但是我猜想,与cond相比,多方法具有巨大的性能开销。如果是这样,为什么我要使用多重方法而不是cond


多种方法允许开放扩展;其他人可以通过在源代码中添加新的defmethod来扩展对任意表达式的多方法分派。他人甚至您自己的代码都不允许对cond表达式进行扩展,而无需编辑cond源。

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


为什么要测量时会担心?

这里是使用标准库的基准样本。 condMulti-methods代码取自http://blog.8thlight.com/myles-megyesi/2012/04/26/polymorphism-in-clojure.html。

注意事项这只是比较multimethodcond性能的基准测试示例。下面的结果表明cond的性能优于multimethod,在实践中不能推广到各种用法。您可以将此基准测试方法用于自己的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;; 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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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

1
2
([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:设计要比每种方法成对出现的纯性能更重要,至少在此示例中如此。