在实验之前,我在 project.clj 中添加了下一行:
:jvm-opts ^:replace [] ; Makes measurements more accurate
基本测量:
(def a (double-array (range 1000000))) ; 10 is too small for performance measurements
(quick-bench (sum-of-squares a)) ; ... Execution time mean : 27.617748 ms ...
(quick-bench (sum-of-squares2 a)) ; ... Execution time mean : 1.259175 ms ...
这或多或少与问题中的时差一致。让我们尝试不使用 Java 数组(这对于 Clojure 来说并不是真正地道的):
(def b (mapv (partial * 1.0) (range 1000000))) ; Persistent vector
(quick-bench (sum-of-squares b)) ; ... Execution time mean : 14.808644 ms ...
快了将近2倍。现在让我们删除类型提示:
(defn sum-of-squares3
"Given a vector v, compute the sum of the squares of elements."
[v]
(r/fold + (r/map #(* % %) v)))
(quick-bench (sum-of-squares3 a)) ; Execution time mean : 30.392206 ms
(quick-bench (sum-of-squares3 b)) ; Execution time mean : 15.583379 ms
与带有类型提示的版本相比,执行时间仅略微增加。顺便说一句,带有传感器的版本具有非常相似的性能并且更清洁:
(defn sum-of-squares3 [v]
(transduce (map #(* % %)) + v))
现在关于附加类型提示。我们确实可以优化第一个sum-of-squares
实现:
(defn square ^double [^double x] (* x x))
(defn sum-of-squares4
"Given a vector v, compute the sum of the squares of elements."
[v]
(r/fold + (r/map square v)))
(quick-bench (sum-of-squares4 b)) ; ... Execution time mean : 12.891831 ms ...
(defn pl
(^double [] 0.0)
(^double [^double x] (+ x))
(^double [^double x ^double y] (+ x y)))
(defn sum-of-squares5
"Given a vector v, compute the sum of the squares of elements."
[v]
(r/fold pl (r/map square v)))
(quick-bench (sum-of-squares5 b)) ; ... Execution time mean : 9.441748 ms ...
注意#1:关于参数和返回值的类型提示sum-of-squares4
并sum-of-squares5
没有额外的性能优势。
注意#2 :从优化开始通常是不好的做法。在大多数情况下,直截了当的版本(apply + (map square v))
将具有足够好的性能。sum-of-squares2
与惯用语相去甚远,并且实际上没有使用 Clojure 概念。如果这真的是性能关键代码 - 最好用 Java 实现它并使用互操作。尽管有 2 种语言,但代码会更干净。或者甚至在非托管代码(C、C++)中实现它并使用 JNI(不是真正可维护的,但如果实现得当,可以提供最佳性能)。