为什么这个Clojure微基准这么慢?

时间:2015-04-05 05:30:01

标签: performance clojure

There was a previous question在比较Clojure与Scala的速度方面得到了成功解决,但将相同的技术应用于以下代码仍然比同等的Scala代码慢25倍。这是将Clojure 1.6.0与Java 1.8.0_40上的Leiningen 2.5.0与Scala 2.11.6进行比较:

比较不使用REPL,而是使用Leiningen“run”命令,并在使用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的项目文件中设置?),因为它们很难在互联网上挖掘出来;对于Scala,我关闭了所有调试输出并启用了编译器“optimize”标志,这有所改进。

我的问题是“是否可以为此类型的应用程序执行某些操作,以使Clojure以与Scala速度相当的速度运行?”。

为了使任何错误推测短路,是的,数组确实被所有二进制数据填充多次,这是由另一系列测试确定的,不,Scala并没有优化掉除了一个循环之外的所有。

我对这两种语言的比较优点的讨论不感兴趣,但只是如何能够产生相当优雅的Clojure代码,以便在一点一点的基础上以相同的速度执行相同的工作至少十倍(不是简单的数组填充操作,因为线性填充只是更复杂的任务的代表,例如素数剔除)。

使用Java BitSet没有问题(但并非所有算法都只适用于一组布尔值),也不可能使用Java Integer数组和Java类方法来访问它,但是应该能够使用它Clojure“本机”数组类型没有这些性能问题。

2 个答案:

答案 0 :(得分:2)

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

所以最后的结果是,你可以通过删除你的类型提示来获得巨大的加速(因为代码中的所有关键部分已经完全明确了类型),并且你可以通过切换{来获得一个小的改进。 {1}}到int(至少在我的64位英特尔机器上)。

答案 1 :(得分:1)

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

在仔细阅读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下。