简体   繁体   English

为什么 Clojure 中的嵌套循环/递归很慢?

[英]Why is nested loop/recur slow in Clojure?

A single loop/recur in Clojure executes as fast as it's Java for loop equivalent Clojure 中的单个循环/重复执行与循环等效的 Java 一样快

Clojure Version: Clojure 版本:

(defn singel-loop [i-count]
  (loop [i 0]
    (if (= i i-count)
      i
      (recur (inc i)))))
(time (loop-test 100101))
"Elapsed time: 0.8857 msecs"

Java Version: Java 版本:

long s = System.currentTimeMillis();
for (i = 0; i < 100000; i++) {
}
System.out.println("Time: " + (System.currentTimeMillis() - s));

Time: ~1ms时间:~1ms

However, if you add an inner loop/recur the performance absolutely falls off of a cliff!但是,如果您添加内部loop/recur ,性能绝对会从悬崖上掉下来!

Clojure: Clojure:

(defn double-loop [i-count j-count]
  (loop [i 0]
    (loop [j 0]
      (if (= j j-count)
        j
        (recur (inc j))))
      (if (= i i-count)
        i
        (recur (inc i)))))
(time (double-loop 100000 100000))
"Elapsed time: 70673.9189 msecs"

Java Version: Java 版本:

long s = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
    for (int j = 0; j < 100000; j++) {
    }
}
System.out.println((System.currentTimeMillis() - s));

Time: ~3ms时间:~3ms

Why does the performance of the Clojure version tank to a comical degree whereas the Java version stays constant?为什么 Clojure 版本的性能下降到可笑的程度,而 Java 版本保持不变?

I think this is primarily due to the Java code being more open to optimization.我认为这主要是由于 Java 代码对优化更加开放。

According to here :根据这里

An infinite loop with an empty body consumes CPU cycles but does nothing.一个空主体的无限循环会消耗 CPU 周期,但什么也不做。 Optimizing compilers and just-in-time systems (JITs) are permitted to (perhaps unexpectedly) remove such a loop.优化编译器和即时系统 (JIT) 被允许(可能出乎意料地)移除这样的循环。 Consequently, programs must not include infinite loops with empty bodies.因此,程序不能包含空体的无限循环。

Although, I can't validate such a claim.虽然,我无法验证这样的说法。 The code here also doesn't involve infinite loops, but empty loops regardless of the exit condition are equally useless.这里的代码也没有涉及无限循环,但是不管退出条件的空循环同样没用。 If anything, a finite loop seems like a more plausible optimization target since at least an infinite loop has a potential purpose (to block indefinitely).如果有的话,有限循环似乎是一个更合理的优化目标,因为至少无限循环有一个潜在的目的(无限期阻塞)。

A better comparison then would be to try to eliminate any such optimization.那么更好的比较是尝试消除任何此类优化。 I chose to use System.out.flush since println can be quite expensive and inconsistent, and I don't thing anything directly affecting System.out.我选择使用System.out.flush ,因为println可能非常昂贵且不一致,而且我不会直接影响System.out. would be optimized out.将被优化出来。

Here are the results:结果如下:

(defn double-loop [i-count j-count]
  (loop [i 0]
    (loop [j 0]
      (if (= j j-count)
        j
        (do
          (.flush System/out)
          (recur (inc j)))))

    (if (= i i-count)
      i

      (recur (inc i)))))

(time (double-loop 1000 10000))  ; "Elapsed time: 1194.718969 msecs"

public class HelloWorld {

     public static void main(String []args){
        long s = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            for (int j = 0; j < 10000; j++) {
                System.out.flush();
            }
        }

        System.out.println((System.currentTimeMillis() - s));  // 1097
     }
}

1194.718969 ms vs 1097 ms 1194.718969 毫秒与 1097 毫秒

So, it seems, potentially, to be a failure of Clojure to compile into easily optimization code.因此,似乎 Clojure 无法轻松编译为优化代码。

Things to note:注意事项:

  • I did these tests on Tutorials Point , not a real environment.我在Tutorials Point上做了这些测试,而不是真实的环境。 IntelliJ has been completely unusable for me since the last update, and I honestly didn't feel like setting up a project for Clojure and fiddling with javac for Java.自从上次更新以来,IntelliJ 对我来说完全无法使用,老实说,我不想为 Clojure 设置项目并为 Java 摆弄javac

  • Why these exact numbers?为什么有这些确切的数字? Because I'm running in a poor environment, and I don't want the website throttling me or doing anything similar.因为我在一个糟糕的环境中运行,我不希望网站限制我或做任何类似的事情。 For whatever reason with the Clojure test, 10000x10000 hung indefinitely (or at least out-did my patience).无论出于何种原因,Clojure 测试,10000x10000 无限期挂起(或者至少超出了我的耐心)。 I had to drop it to 10000x1000 so it would finish.我不得不把它降到 10000x1000 才能完成。

  • As I noted in the comments on the question, this is still an awful way to benchmark languages that run on the JVM as this case shows nicely.正如我在对该问题的评论中指出的那样,这仍然是对在 JVM 上运行的语言进行基准测试的一种糟糕方式,因为这种情况很好地展示了这一点。 See here for why.请参阅此处了解原因。 I use Criterium for Clojure.我对Clojure使用标准。 It's excellent.太棒了。 It runs the code for you before tests to warm everything up, and tries to handle things like garbage collection.它会在测试之前为您运行代码以预热所有内容,并尝试处理垃圾收集之类的事情。

You made it do 100,000 times as much work, and it now takes 100,000 times longer.你让它做 100,000 倍的工作,现在它需要 100,000 倍的时间。 This is not very surprising, and I wouldn't call it "falling off a cliff".这并不奇怪,我也不会称其为“从悬崖上掉下来”。 You might ask why the Java version takes only 3 times as long to do 100,000 times as much work, but at that point it's not really a question about how loop/recur in general performs.您可能会问为什么 Java 版本只需 3 倍的时间就可以完成 100,000 倍的工作,但在这一点上,这并不是关于循环/递归一般如何执行的问题。 Instead it's more a question of what miracle the JIT can pull off with the Java code.相反,更多的问题是 JIT 可以通过 Java 代码实现什么奇迹。

It should raise red flags for you if, as an earlier answer mentioned, a Java nested loop version that appears from the source code that it should require 10,000 times more time than the non-nested loop, only takes 3 times more time (~ 3 msec for the Java nested loop, vs. ~1 msec for the non-nested loop).它应该为您带来危险信号,如前面提到的那样,从源代码中出现的 Java 嵌套循环版本应该比非嵌套循环多 10,000 倍的时间,只需要多 3 倍的时间(~ 3 Java 嵌套循环为毫秒,而非嵌套循环约为 1 毫秒)。 I do not know why that is happening, but a couple of possibilities are:我不知道为什么会这样,但有几个可能性是:

(a) the JVM JIT compilation hasn't kicked in yet for your shorter version, so all or a large fraction of the time is spent interpreting byte code, or executing a less-optimized version of JIT machine code, compared to the nested loop version (a) JVM JIT 编译尚未针对您的较短版本开始,因此与嵌套循环相比,所有或大部分时间都用于解释字节代码或执行不太优化的 JIT 机器代码版本版本

(b) the JVM JIT is somehow determining that your loops do not need to be run, because there is no return value, so the same effect occurs whether the loops are run or not. (b) JVM JIT 以某种方式确定您的循环不需要运行,因为没有返回值,因此无论循环是否运行都会产生相同的效果。 In general, I would recommend doing at least a little bit of computation in each inner loop (eg adding two numbers, eg to a running total), and have a return value that depends upon this computation occurring.一般来说,我建议在每个内部循环中至少进行一点计算(例如,将两个数字相加,例如到运行总数中),并具有取决于此计算发生的返回值。

I have created Clojure and Java versions with similar running times here that you can look at, and recorded the measurement results I obtained using the Criterium library, which runs the same code many times for it to "warm up" the JIT first, and then measures it many more times after that, reporting results based only on the post-warmup executions.我在这里创建了运行时间相似的 Clojure 和 Java 版本,您可以查看,并记录了我使用Criterium库获得的测量结果,该库多次运行相同的代码以先“预热”JIT,然后之后再对其进行多次测量,仅根据预热后的执行情况报告结果。

Java code: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/java/JavaLoops.java Java 代码: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/java/JavaLoops.Z93F725A0742333D1C846FZ44

Clojure code: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/clojure_loops.clj Clojure 代码: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/clojure_loops.clj

Measurement code for both, with results in comments: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/measure_loops.clj两者的测量代码,结果在注释中: https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/measure_loops.clj

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM