您当然可以显着提高此操作的性能。好消息是,您无需为此投入 Java:如果您正确优化 Clojure,它的速度非常快,并且在大多数情况下可以产生与纯 Java 相同的速度。
为了在 Clojure 中实现数字代码的最大性能,您将需要使用:
- arrays,因为您需要具有非常快速的写入和查找的可变存储。Clojure 序列和向量很漂亮,但它们带来了开销,对于真正的性能关键代码,您可能希望避免这些开销
- 双原语,因为它们提供了更快的数学运算。
- aset / aget / areduce - 这些是为数组设计的极其快速的操作,基本上为您提供与纯 Java 等价物相同的字节码。
- 命令式风格——尽管它在 Clojure 中是单调的,但它获得了最快的结果(主要是因为您可以避免内存分配、装箱和函数调用的开销)。一个例子是使用dotimes进行快速的命令式循环。
- (设置!*warn-on-reflection* true) - 并消除代码产生的任何警告,因为反射是一个很大的性能杀手。
以下内容应该是正确的,并且可能会让您获得与 Java 大致相当的性能:
(def kernel (double-array [0 1 1 2 3 3 0 0 0 0 0 0]))
(def data (double-array [1 5 7 4 8 3 9 5 6 3 2 1 1 7 4 9 3 2 1 8 6 4]))
(defn convolve [^doubles kernel-array ^doubles data-array]
(let [ks (count kernel-array)
ds (count data-array)
output (double-array (+ ks ds))
factor (/ 1.0 (areduce kernel-array i ret 0.0 (+ ret (aget kernel-array i))))]
(dotimes [i (int ds)]
(dotimes [j (int ks)]
(let [offset (int (+ i j))]
(aset output offset (+ (aget output offset) (* factor (* (aget data-array i) (aget kernel-array j))))))))
output))
(seq (convolve kernel data))
=> (0.0 0.1 0.6 1.4 2.4 4.4 5.5 6.1000000000000005 5.600000000000001 6.200000000000001 5.499999999999999 5.9 4.199999999999999 3.3000000000000003 2.5 2.2 3.3 4.4 5.6000000000000005 4.8 4.8999999999999995 3.1 3.5 4.300000000000001 5.0 3.0 1.2000000000000002 0.0 0.0 0.0 0.0 0.0 0.0 0.0)
我没有修剪输出数组或做任何边界,所以你可能需要稍微破解这个解决方案才能得到你想要的输出,但希望你能明白......
一些非常粗略的基准测试:
(time (dotimes [i 1000] (seq (convolve kernel data))))
=> "Elapsed time: 8.174109 msecs"
即每个内核/数据对组合大约需要 30ns - 我希望这几乎达到了缓存内存访问的界限。