有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java为什么Clojure中嵌套循环/重复速度慢?

Clojure中的单循环/重复执行速度与Java for循环的执行速度一样快

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版本:

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

时间:~1ms

但是,如果您添加一个内部loop/recur,性能绝对会下降

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版本:

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

时间:~3ms

为什么Clojure版本的性能达到了可笑的程度,而Java版本保持不变


共 (3) 个答案

  1. # 1 楼答案

    你让它做了10万倍的工作,现在需要10万倍的时间。这并不奇怪,我也不会称之为“从悬崖上摔下来”。您可能会问,为什么Java版本只需要3倍的时间就可以完成100000倍的工作,但在这一点上,循环/重现通常如何执行并不是一个真正的问题。相反,问题更多的是JIT能在Java代码中创造出什么奇迹

  2. # 2 楼答案

    我认为这主要是因为Java代码对优化更加开放

    根据here

    An infinite loop with an empty body consumes CPU cycles but does nothing. Optimizing compilers and just-in-time systems (JITs) are permitted to (perhaps unexpectedly) remove such a loop. Consequently, programs must not include infinite loops with empty bodies.

    尽管如此,我无法证实这样的说法。这里的代码也不涉及无限循环,但是不管退出条件如何,空循环都同样无用。如果有什么区别的话,那么有限循环似乎是一个更合理的优化目标,因为至少无限循环有一个潜在的目的(无限期地阻塞)

    一个更好的比较是尝试消除任何此类优化。我选择使用System.out.flush,因为println可能非常昂贵且不一致,而且我认为任何直接影响System.out.的东西都不会被优化掉

    结果如下:

    (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毫秒对1097毫秒

    因此,Clojure可能无法编译成易于优化的代码

    注意事项:

    • 我在Tutorials Point上做了这些测试,而不是在真实的环境中。自从上次更新以来,IntelliJ对我来说已经完全不可用了,老实说,我不想为Clojure设置一个项目,也不想为Java摆弄javac

    • 为什么会有这些确切的数字?因为我在一个糟糕的环境中运行,我不希望网站限制我或做任何类似的事情。不管出于什么原因,在Clojure测试中,10000x1000被无限期地挂起(或者至少让我失去了耐心)。我必须把它降到10000x1000,这样它才能完成

    • 正如我在对这个问题的评论中所指出的那样,这仍然是一种很糟糕的方法来对运行在JVM上的语言进行基准测试,正如本例所示。原因见here。我用Criterium表示Clojure。太棒了。它在测试之前为您运行代码来预热一切,并尝试处理垃圾收集之类的事情

  3. # 3 楼答案

    正如前面提到的答案,如果源代码中显示的Java嵌套循环版本需要比非嵌套循环多10000倍的时间,只需要3倍的时间(Java嵌套循环约3毫秒,而非嵌套循环约1毫秒),那么它应该会给您带来危险。我不知道为什么会发生这种情况,但有几种可能性:

    (a)对于较短的版本,JVM JIT编译尚未开始,因此与嵌套循环版本相比,所有或大部分时间都花在解释字节码或执行JIT机器代码的优化程度较低的版本上

    (b)JVM JIT以某种方式确定不需要运行循环,因为没有返回值,因此无论循环是否运行,都会产生相同的效果。一般来说,我建议在每个内部循环中至少进行一点计算(例如,添加两个数字,例如添加到一个运行总数中),并具有一个取决于此计算发生的返回值

    我在这里创建了运行时间类似的Clojure和Java版本,您可以查看,并记录了我使用Criterium库获得的测量结果,该库多次运行相同的代码,让它先“预热”JIT,然后再对其进行多次测量,仅根据热身后执行情况报告结果

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

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

    两者的测量代码,结果在注释中:https://github.com/jafingerhut/leeuwenhoek/blob/master/src/leeuwenhoek/measure_loops.clj