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 的基准示例。
警告 这只是比较 multimethod
和 cond
性能的基准测试示例。下面的结果表明 cond
比 multimethod
表现更好,不能推广到实践中的各种用法。您可以将此基准测试方法用于您自己的代码。
;; 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 个随机整数的序列,这些整数随机转换为 String
、nil
、keyword
或 vector
。
(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 那样建议:至少在这个例子中,设计比每种方法成对出现的纯性能更重要。
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 的基准示例。
警告 这只是比较 multimethod
和 cond
性能的基准测试示例。下面的结果表明 cond
比 multimethod
表现更好,不能推广到实践中的各种用法。您可以将此基准测试方法用于您自己的代码。
;; 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 个随机整数的序列,这些整数随机转换为 String
、nil
、keyword
或 vector
。
(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 那样建议:至少在这个例子中,设计比每种方法成对出现的纯性能更重要。