【发布时间】:2015-06-09 20:32:55
【问题描述】:
There was a previous question 在比较 Clojure 和 Scala 的速度时得到了成功的回答,但是将这些相同的技术应用于以下代码仍然比等效的 Scala 代码慢 25 倍以上。这是将 Java 1.8.0_40 上的 Clojure 1.6.0 与 Leiningen 2.5.0 与 Scala 2.11.6 进行比较:
比较不是使用 REPL,而是使用 Leiningen“运行”命令,在使用 Leiningen“uberjar”命令生成独立的“.jar”文件后直接从 java 运行时运行速度大致相同。
微基准测试测试在数组内进行位操作的速度,这是一些低级任务类型的典型,例如加密或压缩或素数筛选。为了获得合理的测量间隔并避免 JIT 开销破坏结果,基准测试运行相同的循环 1000 次。
Clojure 代码如下:
(ns test-cljr-speed.core
(:gen-class))
(set! *unchecked-math* true)
(set! *warn-on-reflection* true)
(defn testspeed
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 12),
bits ^int (int (bit-shift-left 1 17))]
(let [buf ^ints(int-array len)]
(letfn [(doit []
(loop [i ^int (int 0)]
(if (< i bits)
(let [w ^int (int (bit-shift-right i 5))]
(do
(aset-int ^ints buf w ^int (int (bit-or ^int (aget ^ints buf w)
^long (bit-shift-left 1 ^long (bit-and i 31)))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit))))))
(defn -main
"runs test."
[& args]
(let [strt (System/nanoTime),
cnt (testspeed),
stop (System/nanoTime)]
(println "Took " (long (/ (- stop strt) 1000000)) " milliseconds.")))
产生以下输出:
Took 9342 milliseconds.
我认为问题与访问缓冲区数组的反射有关,但已按照建议应用了各种类型提示,但似乎找不到。
可比的Scala代码如下:
object Main extends App {
def testspeed() = {
val lps = 1000
val len = 1 << 12
val bits = 1 << 17
val buf = new Array[Int](len)
def doit() = {
def set1(i: Int): Unit =
if (i < bits) {
buf(i >> 5) |= 1 << (i & 31)
set1(i + 1)
}
set1(0)
}
(0 until lps).foreach { _ => doit() }
}
val strt = System.nanoTime()
val cnt = testspeed()
val stop = System.nanoTime()
println(s"Took ${(stop - strt) / 1000000} milliseconds.")
}
产生以下输出:
Took 365 milliseconds.
做同样的工作,速度快25倍以上!!!
我已打开 warn-on-reflection 标志,似乎没有任何 Java 反射正在进行,更多提示会有所帮助。也许我没有正确打开一些优化设置(可能是在 Leiningen 的项目文件中设置的?),因为它们很难在 Internet 上挖掘出来;对于 Scala,我关闭了所有调试输出并启用了编译器“优化”标志,这有了一些改进。
我的问题是“对于这种类型的应用程序可以做些什么来使 Clojure 的运行速度与 Scala 的速度更接近?”。
为了避免任何错误的猜测,是的,数组确实被所有二进制数填充了多次,这是由另一系列测试确定的,不,Scala 并没有优化除一个循环之外的所有内容。
我对讨论两种语言的比较优点不感兴趣,我只关心如何生成相当优雅的 Clojure 代码,以一点一点的速度至少快十倍地完成相同的工作(不是简单的数组填充操作,因为线性填充只是代表更复杂的任务,例如素数剔除)。
使用 Java BitSet 没有问题(但并非所有算法都只适用于一组布尔值),也不太可能使用 Java Integer 数组和 Java 类方法来访问它,但应该能够使用Clojure “本机”数组类型没有这些性能问题。
【问题讨论】:
-
一些注意事项:您在 Clojure 代码中的某些地方提供类型提示绝对不需要它们(例如,
int的结果始终为 int)。如果在绑定点进行提示,则已绑定的中间值和局部变量不需要类型提示。您在两个程序中都使用数字文字,但在 Scala 中它们是整数,在 Clojure 中它们是长整数。只是一种风格:Clojure 的let就像其他 lisps 中的let*一样,您不需要嵌套的 let 块来引用先前的绑定。 -
另外,对于你对无法返回任何其他类型的类型提示和强制函数的所有热情,你在需要它们的地方缺少强制:
(type (inc (int 1))) => java.lang.Long -
@noisesmith,我显然不是 Clojure 方面的专家,我只是随意地向问题抛出类型提示,希望缓慢会消失。如果它减少了执行时间,我会将您的建议编辑到答案代码中,如果它们似乎没有进行更改,则删除类型提示。我知道数字整数文字都是长整数并且不用担心它,因为在 64 位机器上长计算不会比 int 花费更长的时间,但是指定 Int 用于用作索引的 Scala 变量,因为它们使用 Java 限制数组索引必须是 Int。感谢您指出这些。
-
Clojure 还使用了数组索引必须是 int 的 Java 限制,并且它隐式地进行了这种转换(一种相对便宜的转换)。
-
@noisesmith,是的,我认为让 Clojure 编译器决定何时进行 int 转换而不是通过显式强制转换可能会更有效。
标签: performance clojure