为什么这个 Clojure 微型基准测试这么慢?
Why is this Clojure micro benchmark so slow?
There was a previous question 在比较 Clojure 和 Scala 的速度时成功回答了这个问题,但是将这些相同的技术应用于以下代码仍然比等效的 Scala 代码慢 25 倍以上。这是在 Java 1.8.0_40 和 Scala 2.11.6:
上比较 Clojure 1.6.0 和 Leiningen 2.5.0
不使用 REPL 而是使用 Leiningen "run" 命令和 运行 以大约相同的速度进行比较,当 运行 直接从 java 生成一个使用 Leiningen "uberjar" 命令的独立“.jar”文件。
微基准测试在数组内进行位操作的速度,这是一些低级任务类型的典型,例如加密或压缩或素数筛选。为了获得合理的测量间隔并避免 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倍以上!!!
我已经打开 反射警告 标志,但似乎没有任何 Java 反射在更多提示会有所帮助的地方进行。也许我没有正确打开一些优化设置(也许在 Leiningen 的项目文件中设置?),因为它们很难在 Internet 上挖掘出来;对于 Scala,我关闭了所有调试输出并启用了编译器 "optimize" 标志,这带来了一些改进。
我的问题是“对于这种类型的应用程序,是否可以做些什么来使 Clojure 运行 的速度与 Scala 速度相媲美? ".
为了避免任何错误的推测,是的,根据另一系列测试的确定,数组确实被所有二进制数填充了多次,不,Scala 除了一个循环之外并没有优化所有内容。
我对讨论这两种语言的优点比较不感兴趣,只想讨论如何生成相当优雅的 Clojure 代码,在一点一点的基础上至少快十倍的速度完成同样的工作(不是简单的数组填充操作,因为线性填充只是代表更复杂的任务,例如素数剔除)。
使用 Java BitSet 没有问题(但并非所有算法都只适用于一组布尔值),也不太可能使用 Java 整数数组和 Java class 方法来访问它,但应该能够使用 Clojure "native" 数组类型而不会出现这些性能问题。
我将只回答我自己的问题,以帮助可能遇到同样问题的其他人:
仔细阅读another question's answer后,无意中发现了问题:"aset"可以; "aset-int"(以及 "aset-?" 的所有其他特殊形式)不是,类型提示再多也无济于事。
在下面的测试程序代码中 根据@noisesmith 的回答编辑,我所做的更改是使用 "long-array"("int array" 也可以,只是没那么快)并使用 "aset" 而不是 "aset-long"(或 "aset-int" 代替 "int-array")并消除了所有类型提示:
(set! *unchecked-math* true)
(defn testspeed
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 11),
bits (bit-shift-left 1 17),
buf (long-array len)]
(letfn [(doit []
(loop [i (int 0)]
(if (< i bits)
(let [w (bit-shift-right i 6)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 63))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))
结果是它产生以下输出:
Took 395 milliseconds.
用"aset-long"代替"aset",输出是:
Took 7424 milliseconds.
提速近 19 倍。
现在这比使用 Int 数组的 Scala 代码稍微慢一点(对于 Scala 来说比使用 Long 数组更快),但这在某种程度上是可以理解的,因为 Clojure 没有 read/modify/write 原语作为“|=”,编译器似乎不够聪明,无法看到上述代码中隐含的 read/modify/write 操作。
但是,只慢几个百分点是完全可以接受的,这意味着对于此类应用程序,性能不是在 Scala 或 Clojure 之间进行选择的标准。
这个解决方案没有意义,因为 "aset-?" 的特殊版本实际上应该只是调用 "aset" 的重载情况,但似乎有一个 problem/bug 影响他们的表现,至少在当前版本 1.6.0.
首先,您的类型提示不会影响 Clojure 代码的执行时间,并且在我的机器上更新版本没有改进:
user=> (time (testspeed))
"Elapsed time: 6256.075155 msecs"
nil
user=> (time (testspeedx))
"Elapsed time: 6371.968782 msecs"
nil
您正在做一些不需要的类型提示,将它们全部剥离实际上会使代码更快:
(defn testspeed-unhinted
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 12),
bits (bit-shift-left 1 17)]
(let [buf (int-array len)]
(letfn [(doit []
(loop [i (int 0)]
(if (< i bits)
(let [w (bit-shift-right i 5)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 31))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))))
user=> (time (testspeed-unhinted))
"Elapsed time: 270.652953 msecs"
我想到在 recur 上强制 i
为 int 可能会加快代码速度,但实际上会减慢速度。考虑到这一点,我决定尝试从代码中完全删除 int
s 并查看结果在性能方面的表现:
(defn testspeed-unhinted-longs
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 12),
bits (bit-shift-left 1 17)]
(let [buf (long-array len)]
(letfn [(doit []
(loop [i 0]
(if (< i bits)
(let [w (bit-shift-right i 5)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 31))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))))
user=> (time (testspeed-unhinted-longs))
"Elapsed time: 221.025048 msecs"
性能提升相对较小,所以我使用 criterium
库来获得准确的微基准测试差异:
user=> (crit/bench (testspeed-unhinted))
WARNING: Final GC required 2.2835076167941852 % of runtime
Evaluation count : 240 in 60 samples of 4 calls.
Execution time mean : 260.877321 ms
Execution time std-deviation : 18.168141 ms
Execution time lower quantile : 251.952111 ms ( 2.5%)
Execution time upper quantile : 321.995872 ms (97.5%)
Overhead used : 15.568045 ns
Found 8 outliers in 60 samples (13.3333 %)
low-severe 1 (1.6667 %)
low-mild 7 (11.6667 %)
Variance from outliers : 51.8061 % Variance is severely inflated by outliers
nil
user=> (crit/bench (testspeed-unhinted-longs))
Evaluation count : 300 in 60 samples of 5 calls.
Execution time mean : 232.078704 ms
Execution time std-deviation : 24.828378 ms
Execution time lower quantile : 219.615718 ms ( 2.5%)
Execution time upper quantile : 297.456135 ms (97.5%)
Overhead used : 15.568045 ns
Found 11 outliers in 60 samples (18.3333 %)
low-severe 2 (3.3333 %)
low-mild 9 (15.0000 %)
Variance from outliers : 72.1097 % Variance is severely inflated by outliers
nil
所以最后的结果是,你可以通过删除你的类型提示来获得巨大的加速(因为代码中的所有关键内容已经完全明确的类型),你可以通过切换来获得一个小的改进从 int
到 long
(至少在我的 64 位英特尔机器上)。
There was a previous question 在比较 Clojure 和 Scala 的速度时成功回答了这个问题,但是将这些相同的技术应用于以下代码仍然比等效的 Scala 代码慢 25 倍以上。这是在 Java 1.8.0_40 和 Scala 2.11.6:
上比较 Clojure 1.6.0 和 Leiningen 2.5.0不使用 REPL 而是使用 Leiningen "run" 命令和 运行 以大约相同的速度进行比较,当 运行 直接从 java 生成一个使用 Leiningen "uberjar" 命令的独立“.jar”文件。
微基准测试在数组内进行位操作的速度,这是一些低级任务类型的典型,例如加密或压缩或素数筛选。为了获得合理的测量间隔并避免 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倍以上!!!
我已经打开 反射警告 标志,但似乎没有任何 Java 反射在更多提示会有所帮助的地方进行。也许我没有正确打开一些优化设置(也许在 Leiningen 的项目文件中设置?),因为它们很难在 Internet 上挖掘出来;对于 Scala,我关闭了所有调试输出并启用了编译器 "optimize" 标志,这带来了一些改进。
我的问题是“对于这种类型的应用程序,是否可以做些什么来使 Clojure 运行 的速度与 Scala 速度相媲美? ".
为了避免任何错误的推测,是的,根据另一系列测试的确定,数组确实被所有二进制数填充了多次,不,Scala 除了一个循环之外并没有优化所有内容。
我对讨论这两种语言的优点比较不感兴趣,只想讨论如何生成相当优雅的 Clojure 代码,在一点一点的基础上至少快十倍的速度完成同样的工作(不是简单的数组填充操作,因为线性填充只是代表更复杂的任务,例如素数剔除)。
使用 Java BitSet 没有问题(但并非所有算法都只适用于一组布尔值),也不太可能使用 Java 整数数组和 Java class 方法来访问它,但应该能够使用 Clojure "native" 数组类型而不会出现这些性能问题。
我将只回答我自己的问题,以帮助可能遇到同样问题的其他人:
仔细阅读another question's answer后,无意中发现了问题:"aset"可以; "aset-int"(以及 "aset-?" 的所有其他特殊形式)不是,类型提示再多也无济于事。
在下面的测试程序代码中 根据@noisesmith 的回答编辑,我所做的更改是使用 "long-array"("int array" 也可以,只是没那么快)并使用 "aset" 而不是 "aset-long"(或 "aset-int" 代替 "int-array")并消除了所有类型提示:
(set! *unchecked-math* true)
(defn testspeed
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 11),
bits (bit-shift-left 1 17),
buf (long-array len)]
(letfn [(doit []
(loop [i (int 0)]
(if (< i bits)
(let [w (bit-shift-right i 6)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 63))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))
结果是它产生以下输出:
Took 395 milliseconds.
用"aset-long"代替"aset",输出是:
Took 7424 milliseconds.
提速近 19 倍。
现在这比使用 Int 数组的 Scala 代码稍微慢一点(对于 Scala 来说比使用 Long 数组更快),但这在某种程度上是可以理解的,因为 Clojure 没有 read/modify/write 原语作为“|=”,编译器似乎不够聪明,无法看到上述代码中隐含的 read/modify/write 操作。
但是,只慢几个百分点是完全可以接受的,这意味着对于此类应用程序,性能不是在 Scala 或 Clojure 之间进行选择的标准。
这个解决方案没有意义,因为 "aset-?" 的特殊版本实际上应该只是调用 "aset" 的重载情况,但似乎有一个 problem/bug 影响他们的表现,至少在当前版本 1.6.0.
首先,您的类型提示不会影响 Clojure 代码的执行时间,并且在我的机器上更新版本没有改进:
user=> (time (testspeed))
"Elapsed time: 6256.075155 msecs"
nil
user=> (time (testspeedx))
"Elapsed time: 6371.968782 msecs"
nil
您正在做一些不需要的类型提示,将它们全部剥离实际上会使代码更快:
(defn testspeed-unhinted
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 12),
bits (bit-shift-left 1 17)]
(let [buf (int-array len)]
(letfn [(doit []
(loop [i (int 0)]
(if (< i bits)
(let [w (bit-shift-right i 5)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 31))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))))
user=> (time (testspeed-unhinted))
"Elapsed time: 270.652953 msecs"
我想到在 recur 上强制 i
为 int 可能会加快代码速度,但实际上会减慢速度。考虑到这一点,我决定尝试从代码中完全删除 int
s 并查看结果在性能方面的表现:
(defn testspeed-unhinted-longs
"test array bit manipulating tight loop speeds."
[]
(let [lps 1000,
len (bit-shift-left 1 12),
bits (bit-shift-left 1 17)]
(let [buf (long-array len)]
(letfn [(doit []
(loop [i 0]
(if (< i bits)
(let [w (bit-shift-right i 5)]
(do
(aset buf w (bit-or (aget buf w)
(bit-shift-left 1 (bit-and i 31))))
(recur (inc i)))))))]
(dorun lps (repeatedly doit)))))))
user=> (time (testspeed-unhinted-longs))
"Elapsed time: 221.025048 msecs"
性能提升相对较小,所以我使用 criterium
库来获得准确的微基准测试差异:
user=> (crit/bench (testspeed-unhinted))
WARNING: Final GC required 2.2835076167941852 % of runtime
Evaluation count : 240 in 60 samples of 4 calls.
Execution time mean : 260.877321 ms
Execution time std-deviation : 18.168141 ms
Execution time lower quantile : 251.952111 ms ( 2.5%)
Execution time upper quantile : 321.995872 ms (97.5%)
Overhead used : 15.568045 ns
Found 8 outliers in 60 samples (13.3333 %)
low-severe 1 (1.6667 %)
low-mild 7 (11.6667 %)
Variance from outliers : 51.8061 % Variance is severely inflated by outliers
nil
user=> (crit/bench (testspeed-unhinted-longs))
Evaluation count : 300 in 60 samples of 5 calls.
Execution time mean : 232.078704 ms
Execution time std-deviation : 24.828378 ms
Execution time lower quantile : 219.615718 ms ( 2.5%)
Execution time upper quantile : 297.456135 ms (97.5%)
Overhead used : 15.568045 ns
Found 11 outliers in 60 samples (18.3333 %)
low-severe 2 (3.3333 %)
low-mild 9 (15.0000 %)
Variance from outliers : 72.1097 % Variance is severely inflated by outliers
nil
所以最后的结果是,你可以通过删除你的类型提示来获得巨大的加速(因为代码中的所有关键内容已经完全明确的类型),你可以通过切换来获得一个小的改进从 int
到 long
(至少在我的 64 位英特尔机器上)。