【问题标题】:Why is this Clojure micro benchmark so slow?为什么这个 Clojure 微基准测试这么慢?
【发布时间】: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))) =&gt; java.lang.Long
  • @noisesmith,我显然不是 Clojure 方面的专家,我只是随意地向问题抛出类型提示,希望缓慢会消失。如果它减少了执行时间,我会将您的建议编辑到答案代码中,如果它们似乎没有进行更改,则删除类型提示。我知道数字整数文字都是长整数并且不用担心它,因为在 64 位机器上长计算不会比 int 花费更长的时间,但是指定 Int 用于用作索引的 Scala 变量,因为它们使用 Java 限制数组索引必须是 Int。感谢您指出这些。
  • Clojure 还使用了数组索引必须是 int 的 Java 限制,并且它隐式地进行了这种转换(一种相对便宜的转换)。
  • @noisesmith,是的,我认为让 Clojure 编译器决定何时进行 int 转换而不是通过显式强制转换可能会更有效。

标签: performance clojure


【解决方案1】:

首先,您的类型提示不会影响 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"

在我看来,强制 i 在 recur 上转换为 int 可能会加快代码速度,但实际上会减慢它的速度。考虑到这一点,我决定尝试从代码中完全删除 ints 并查看性能方面的结果:

 (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 lib 来获得准确的微基准测试差异:

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

所以最终结果是,您可以通过删除类型提示来获得巨大的加速(因为代码中的所有关键内容已经完全明确类型),并且您可以通过从 @ 切换来获得一个小的改进987654328@ 到 long(至少在我的 64 位英特尔机器上)。

【讨论】:

  • 看起来我一直在解决的问题完全是由于“aset-int”的性能错误,没有类型提示有帮助,就像我根据你的代码删除所有类型提示但是使用“aset-int”而不是“aset”(以及需要强制强制转换为 int 的那个),性能恢复到以前的样子 - 很糟糕。我在 64 位 AMD Bulldozer 上,由于 L1 缓存瓶颈,运行速度慢了 2 倍。由于必须使用 32 位 reg 模拟 64 位操作,在 32 位机器上使用 long 可能会更慢。感谢您的帮助。
  • 使用上面的“长数组”意味着需要调整一些移位,例如在定义“len”时将移位设为 11(以保持数组大小相同,以字节为单位),从 5 右移到 6,另一个左移用“& 63”而不是“& 31”,否则一半的数组缓冲区没有被使用。在我的机器上,Scala 使用 int 数组更快,但 Clojure 使用 long 数组(每个字节大小相同)更快 - 可能是由于默认情况下每个使用的“本机”原始类型。感谢您指向标准库;不知道。
  • 在编辑我的答案时,我明白了为什么上次编辑后没有改进:我在不同的试用版中进行了编辑,而不是反映您的建议的版本,所以难怪您的第一次比较仍然有性能问题:那个时候我使用的是“aset-boolean”,它仍然有这个性能错误。新版本对您上面建议的缓冲区大小略有改进。
【解决方案2】:

我只会回答我自己的问题,以帮助可能正在解决同样问题的其他人:

看了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 没有读/修改/写原语“ |=" 并且似乎编译器不够聪明,无法看到上面代码中隐含的读/修改/写操作。

但是,仅慢几个百分点是完全可以接受的,这意味着对于这种类型的应用程序,性能不是在 Scala 或 Clojure 之间进行选择的标准。

这个解决方案没有意义,因为“aset-?”的特殊版本实际上应该只是调用“aset”的重载情况,但似乎存在影响其性能的问题/错误,至少在当前版本 1.6.0 中是这样。

【讨论】:

  • 这给了我:IllegalArgumentException Value out of range for int: 4294967295 clojure.lang.RT.intCast
  • @noisesmith,您需要问题代码中的“(set!unchecked-math true)”;这只是关于“testspeed”过程的修改代码。如果根据问题中的 cmets 所需的实际强制转换有任何速度差异,我将对其进行编辑
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-27
  • 2023-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多