Clojure 性能,如何输入提示到 r/map

Posted

技术标签:

【中文标题】Clojure 性能,如何输入提示到 r/map【英文标题】:Clojure Performance, How to Type hint to r/map 【发布时间】:2015-03-26 14:02:58 【问题描述】:

下面,我有 2 个函数计算它们的参数的平方和。第一个很好用,但比第二个慢 20 倍。我认为 r/map 没有利用 get 从双数组中检索元素,而我在函数 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 纳秒

【问题讨论】:

【参考方案1】:

在实验之前,我在 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-squares4sum-of-squares5 的参数和返回值的类型提示没有额外的性能优势。

注意#2:以optimizations 开头通常是不好的做法。直截了当的版本(apply + (map square v)) 在大多数情况下都具有足够好的性能。 sum-of-squares2 与惯用语相去甚远,实际上没有使用 Clojure 概念。如果这真的是性能关键代码 - 最好用 Java 实现它并使用互操作。尽管有 2 种语言,但代码会更简洁。甚至在非托管代码(C、C++)中实现它并使用 JNI(不是真正可维护的,但如果实现得当,可以提供最佳性能)。

【讨论】:

谢谢。我知道我的 v2 不是惯用的,但代码对性能非常敏感(数万亿次计算),每次我有一个热代码补丁时,我都不想使用 java。我自然更喜欢使用通用的 clojure-esque 版本,但即使是 10:1 的性能下降也是相当显着的。因此,对于这个特定的应用程序,我将坚持使用我的 v2。我只是想把我的蛋糕也吃掉……用你的传感器 v3 的优雅来接近 v2 的性能。 换句话说,我将 v2 视为一个互操作,没有 2 种语言的开销。【参考方案2】:

为什么不使用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-uncheckedmultiply-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 毫秒。

【讨论】:

谢谢罗德里戈。我不知道arereduce。这正是我所需要的,一种告诉减少使用 get...

以上是关于Clojure 性能,如何输入提示到 r/map的主要内容,如果未能解决你的问题,请参考以下文章

Clojure : 类型提示塔

在 Clojure 中类型提示一个 nil 文字

Clojure 类型提示,无法解析类名 clojure.core$double

卡在 clojure 中的泛型类类型提示

Clojure 中函数的类型提示

类型提示存储为clojure中的元数据?