Clojure 性能,如何键入 r/map 的提示
Clojure Performance, How to Type hint to r/map
下面,我有 2 个函数计算它们参数的平方和。第一个很好用,但比第二个慢 20 倍。我假设 r/map 没有利用 aget 从双数组中检索元素,而我在函数 2 中明确地这样做了。
有什么方法可以进一步提示或帮助 r/map r/fold 执行得更快?
(defn sum-of-squares
"Given a vector v, compute the sum of the squares of elements."
^double [^doubles v]
(r/fold + (r/map #(* % %) v)))
(defn sum-of-squares2
"This is much faster than above. Post to stack-overflow to see."
^double [^doubles v]
(loop [val 0.0
i (dec (alength v))]
(if (neg? i)
val
(let [x (aget v i)]
(recur (+ val (* x x)) (dec i))))))
(def a (double-array (range 10)))
(quick-bench (sum-of-squares a))
800 纳秒
(quick-bench (sum-of-squares2 a))
40 纳秒
在实验之前,我在 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
与带有类型提示的版本相比,执行时间仅略有增加。顺便说一下,带有 transducers 的版本具有非常相似的性能并且更干净:
(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
的参数和 return 值的类型提示没有额外的性能优势。
注 #2:通常以 optimizations 开头是不好的做法。对于大多数情况,直接版本 (apply + (map square v))
将具有足够好的性能。 sum-of-squares2
与惯用语相去甚远,实际上没有使用 Clojure 概念。如果这确实是性能关键代码 - 最好在 Java 中实现它并使用互操作。尽管有 2 种语言,代码会更清晰。或者甚至在非托管代码(C、C++)中实现它并使用 JNI(不是真正可维护的,但如果正确实现,可以提供最佳性能)。
为什么不用areduce
:
(def sum-of-squares3 ^double [^doubles v]
(areduce v idx ret 0.0
(let [item (aget v idx)]
(+ ret (* item item)))))
在我的机器上 运行:
(criterium/bench (sum-of-squares3 (double-array (range 100000))))
给出的平均执行时间为 1.809103 毫秒,您的 sum-of-squares2
执行相同的计算需要 1.455775 毫秒。我认为这个使用 areduce
的版本比你的版本更地道。
为了进一步提高性能,您可以尝试使用未经检查的数学 (add-unchecked
and multiply-unchecked
)。但是要注意,你需要确保你的计算不会溢出:
(defn sum-of-squares4 ^double [^doubles v]
(areduce v idx ret 0.0
(let [item (aget v idx)]
(unchecked-add ret (unchecked-multiply item item)))))
运行 相同的基准测试给出的平均执行时间为 1.144197 毫秒。您的 sum-of-squares2
还可以从平均执行时间为 1.126001 毫秒的未经检查的数学中受益。
下面,我有 2 个函数计算它们参数的平方和。第一个很好用,但比第二个慢 20 倍。我假设 r/map 没有利用 aget 从双数组中检索元素,而我在函数 2 中明确地这样做了。
有什么方法可以进一步提示或帮助 r/map r/fold 执行得更快?
(defn sum-of-squares
"Given a vector v, compute the sum of the squares of elements."
^double [^doubles v]
(r/fold + (r/map #(* % %) v)))
(defn sum-of-squares2
"This is much faster than above. Post to stack-overflow to see."
^double [^doubles v]
(loop [val 0.0
i (dec (alength v))]
(if (neg? i)
val
(let [x (aget v i)]
(recur (+ val (* x x)) (dec i))))))
(def a (double-array (range 10)))
(quick-bench (sum-of-squares a))
800 纳秒
(quick-bench (sum-of-squares2 a))
40 纳秒
在实验之前,我在 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
与带有类型提示的版本相比,执行时间仅略有增加。顺便说一下,带有 transducers 的版本具有非常相似的性能并且更干净:
(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
的参数和 return 值的类型提示没有额外的性能优势。
注 #2:通常以 optimizations 开头是不好的做法。对于大多数情况,直接版本 (apply + (map square v))
将具有足够好的性能。 sum-of-squares2
与惯用语相去甚远,实际上没有使用 Clojure 概念。如果这确实是性能关键代码 - 最好在 Java 中实现它并使用互操作。尽管有 2 种语言,代码会更清晰。或者甚至在非托管代码(C、C++)中实现它并使用 JNI(不是真正可维护的,但如果正确实现,可以提供最佳性能)。
为什么不用areduce
:
(def sum-of-squares3 ^double [^doubles v]
(areduce v idx ret 0.0
(let [item (aget v idx)]
(+ ret (* item item)))))
在我的机器上 运行:
(criterium/bench (sum-of-squares3 (double-array (range 100000))))
给出的平均执行时间为 1.809103 毫秒,您的 sum-of-squares2
执行相同的计算需要 1.455775 毫秒。我认为这个使用 areduce
的版本比你的版本更地道。
为了进一步提高性能,您可以尝试使用未经检查的数学 (add-unchecked
and multiply-unchecked
)。但是要注意,你需要确保你的计算不会溢出:
(defn sum-of-squares4 ^double [^doubles v]
(areduce v idx ret 0.0
(let [item (aget v idx)]
(unchecked-add ret (unchecked-multiply item item)))))
运行 相同的基准测试给出的平均执行时间为 1.144197 毫秒。您的 sum-of-squares2
还可以从平均执行时间为 1.126001 毫秒的未经检查的数学中受益。