为什么对同一方法的两次连续调用会产生不同的执行时间?

时间:2009-04-29 22:48:31

标签: java benchmarking microbenchmark

以下是示例代码:

public class TestIO{
public static void main(String[] str){
    TestIO t = new TestIO();
    t.fOne();
    t.fTwo();
    t.fOne();
    t.fTwo();
}


public void fOne(){
    long t1, t2;
    t1 = System.nanoTime();
    int i = 10;
    int j = 10;
    int k = j*i;
    System.out.println(k);
    t2 = System.nanoTime();
    System.out.println("Time taken by 'fOne' ... " + (t2-t1));
}

public void fTwo(){
    long t1, t2;
    t1 = System.nanoTime();
    int i = 10;
    int j = 10;
    int k = j*i;
    System.out.println(k);
    t2 = System.nanoTime();
    System.out.println("Time taken by 'fTwo' ... " + (t2-t1));
}

}

这给出了以下输出:     100     'fOne'所花费的时间...... 390273     100     'fTwo'所花费的时间...... 118451     100     'fOne'所花费的时间...... 53359     100     'fTwo'所花费的时间...... 115936     按任意键继续 。 。

为什么第一次执行相同方法比连续调用需要更多时间(明显更多)?

我尝试将-XX:CompileThreshold=1000000提供给命令行,但没有区别。

8 个答案:

答案 0 :(得分:7)

有几个原因。 JIT(即时)编译器可能没有运行。 JVM可以执行不同调用之间的优化。您正在测量经过的时间,因此您的计算机上可能正在运行Java之外的其他内容。处理器和RAM缓存在随后的调用中可能“温暖”。

您确实需要进行多次调用(数千次)以获得准确的每个方法执行时间。

答案 1 :(得分:7)

Andreas提到的问题和JIT的不可预测性是正确的,但还有一个问题是类加载器

fOne的第一次调用与后者的调用完全不同,因为这是第一次调用System.out.println,这意味着类加载器将从磁盘或文件系统缓存中调用(通常它是缓存的)打印文本所需的所有类。将参数-verbose:class提供给JVM,以查看在这个小程序中实际加载了多少个类。

我在运行单元测试时注意到了类似的行为 - 调用大型框架的第一个测试需要更长的时间(如果Guice在C2Q6600上大约需要250ms),即使测试代码是相同的,因为第一个调用是指类加载器加载数百个类。

由于您的示例程序太短,因此开销可能来自非常早期的JIT优化和类加载活动。垃圾收集器甚至可能在程序结束之前就没有启动。


<强>更新

现在我找到了一种可靠的方法来找出真正花费时间的东西。还没有人发现它,虽然它与类加载密切相关 - 它是原生方法的动态链接

我按如下方式修改了代码,以便日志在测试开始和结束时显示(通过查看何时加载这些空标记类)。

    TestIO t = new TestIO();
    new TestMarker1();
    t.fOne();
    t.fTwo();
    t.fOne();
    t.fTwo();
    new TestMarker2();

运行程序的命令,右侧JVM parameters显示正在发生的事情:

java -verbose:class -verbose:jni -verbose:gc -XX:+PrintCompilation TestIO

输出:

* snip 493 lines *
[Loaded java.security.Principal from shared objects file]
[Loaded java.security.cert.Certificate from shared objects file]
[Dynamic-linking native method java.lang.ClassLoader.defineClass1 ... JNI]
[Loaded TestIO from file:/D:/DEVEL/Test/classes/]
  3       java.lang.String::indexOf (166 bytes)
[Loaded TestMarker1 from file:/D:/DEVEL/Test/classes/]
[Dynamic-linking native method java.io.FileOutputStream.writeBytes ... JNI]
100
Time taken by 'fOne' ... 155354
100
Time taken by 'fTwo' ... 23684
100
Time taken by 'fOne' ... 22672
100
Time taken by 'fTwo' ... 23954
[Loaded TestMarker2 from file:/D:/DEVEL/Test/classes/]
[Loaded java.util.AbstractList$Itr from shared objects file]
[Loaded java.util.IdentityHashMap$KeySet from shared objects file]
* snip 7 lines *

时差的原因是:[Dynamic-linking native method java.io.FileOutputStream.writeBytes ... JNI]

我们还可以看到,JIT编译器不会影响此基准测试。只编译了三种方法(例如上面代码段中的java.lang.String::indexOf),它们都是在调用fOne方法之前发生的。

答案 2 :(得分:5)

  1. 测试的代码非常简单。采取的最昂贵的行动是

     System.out.println(k);
    

    所以你要测量的是调试输出的写入速度。这种情况差异很大,甚至可能取决于屏幕上调试窗口的位置,如果需要滚动其大小等等。

  2. JIT / Hotspot逐步优化常用的代码路径。

  3. 处理器优化预期的代码路径。使用的路径更经常执行得更快。

  4. 您的样本量太小了。这样的微基准测试通常会进行预热阶段,您可以看到应该像Java is really fast at doing nothing那样广泛地完成这项工作。

答案 3 :(得分:3)

除了JITting,其他因素可能是:

  • 调用System.out.println
  • 时进程的输出流阻塞
  • 您的流程已由其他流程安排出来
  • 垃圾收集器在后台线程上做一些工作

如果你想获得好的基准,你应该

  • 运行您要进行基准测试的代码,至少数千次,并计算平均时间。
  • 忽略前几次通话的次数(由于JITting等)
  • 如果可以,请禁用GC;如果您的代码生成大量对象,这可能不是一个选项。
  • 从正在进行基准测试的代码中取出日志记录(println调用)。

在几个平台上有基准测试库可以帮助你完成这些工作;他们还可以计算标准偏差和其他统计数据。

答案 4 :(得分:2)

最可能的罪魁祸首是JIT(即时)热点引擎。基本上第一次执行代码时,JVM会“记住”机器代码,然后在将来的执行中重复使用。

答案 5 :(得分:1)

我认为这是因为第一次运行后第二次生成的代码已经过优化。

答案 6 :(得分:1)

正如所建议的那样,JIT可能是罪魁祸首,但如果机器上的其他进程当时正在使用资源,那么I / O等待时间以及资源等待时间也是如此。

这个故事的寓意是,微基准测试是一个难题,特别是对于Java。我不知道你为什么要这样做,但如果你试图在两种方法中选择一个问题,不要这样测量它们。使用策略设计模式,使用两种不同的方法运行整个程序并测量整个系统。即使从长远来看,这也会使处理时间出现小问题,并且可以让您更加真实地了解整个应用程序的性能在这一点上的瓶颈(提示:它可能比您想象的要小。)

答案 7 :(得分:1)

最可能的答案是初始化。 JIT肯定不是正确的答案,因为它在开始优化之前需要更多的周期。但在第一时间可能会出现:

  • 查找类(缓存,因此不需要第二次查找)
  • 加载类(一旦加载留在内存中)
  • 从本机库获取其他代码(本机代码已缓存)
  • 最后它加载要在CPU的L1缓存中执行的代码。对于你的意义上的加速,这是最可行的情况,同时基准(作为微基准测试)的原因并不多。如果你的代码足够小,循环的第二次调用可以从CPU内部完全运行,这很快。在现实世界中,这种情况不会发生,因为程序更大,而且L1缓存的重用远远不是那么大。