为什么这个 Clojure 代码与 Java 中的替代代码相比这么慢?

Why is this Clojure code so slow compared to the alternative in Java?

tl;dr:为什么下面的代码这么慢?

我尝试优化以下代码以提高速度;它的目的是在执行 n^2 操作时将一个数组(大小 n=1000)转换为另一个(相同大小),转换的细节现在并不重要。

因为我试图获得尽可能快的速度,所以我尽可能地使用 Java 原语;不过,我得到的结果通常是每次 'transform' 调用大约 70 毫秒。重写为 Java 时,平均调用耗时 < 2 毫秒。

1) 哇,Java 真快

2) 哇,Clojure 很慢

3) 你能给我解释一下,为什么会这样?天真地,我希望 Clojure 生成代码字节码,应该非常接近 Java,为什么不是这样?

4) 我不是 100% 确定如何使用这些 ^ints 提示,也许我弄错了?

(defn transform [^ints src]
  (let [res ^ints (make-array Integer/TYPE 1000)]
    (loop [x (int 0)]
      (if (= 1000 x) res
        (do
          (aset res x (areduce src i ret (int 0) 
            (+ ret (* (mod x 2) (mod i 3) (aget src i)))))
          (recur (inc x)))))))

(let [arr (into-array Integer/TYPE (range 1000))]
  (doseq [_ (range 20)]
      (println (time (transform arr)))
  ))

像这样的东西应该更接近:

(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)

(defn inner ^long [^ints src ^long x]
  (let [len (alength src)]
    (loop [i 0 acc 0]
      (if (< i len)
        (recur (inc i) (+ acc (* (rem x 2) (rem i 3) (aget src i))))
        acc))))

(defn transform [^ints src]
  (let [res ^ints (int-array 1000)]
    (loop [x 0]
      (if (= 1000 x) 
        res
          (do
            (aset res x (inner src x))
            (recur (inc x)))))))

(defn bench []
  (let [arr (int-array (range 1000))]
    (doseq [_ (range 20)]
      (println (time (transform arr))))))

顶部设置有助于检测错误。 :warn-on-boxed 是 Clojure 1.7 中的新功能(目前处于 beta1,尚未完全推出),但在这里特别有用。

我改变了一些重要的事情:

  • 我替换了 areduce - reduce 的问题是它不知道数组的原始类型。通过编写自己的内部循环,您可以利用这些提示。可以暗示 reduce 的主体使其工作,但在进行原始数学时我倾向于更喜欢显式循环。
  • 我在需要的地方使用 ^long 提示,因为 Clojure 只支持原始 long param/return,不支持 int。将根据需要插入正确的原始转换。如果需要,有一些函数可以获取原始 int 溢出语义。
  • rem 可以转到 mod 不能的原始操作。我认为这里的语义对于您正在做的事情是相同的。这是大部分拳击的来源。
  • 我正在使用 int-array 而不是您制作数组的其他方法。我认为这是您所做工作的最佳方式。

您可以将两个循环合二为一,进一步提高性能。