如何确保不会发生jvm和编译器优化

我有这个代码测试Calendar.getInstance().getTimeInMillis() vs System.currentTimeMilli()

 long before = getTimeInMilli(); for (int i = 0; i < TIMES_TO_ITERATE; i++) { long before1 = getTimeInMilli(); doSomeReallyHardWork(); long after1 = getTimeInMilli(); } long after = getTimeInMilli(); System.out.println(getClass().getSimpleName() + " total is " + (after - before)); 

我想确保没有JVM或编译器优化发生,因此测试将是有效的,并且实际上会显示差异。

怎么样?

编辑 :我改变了代码示例,以便更清楚。 我在这里检查的是在不同的实现中调用getTimeInMilli()需要多长时间 – Calendar vs System。

我想你需要禁用JIT。 添加到您的运行命令下一个选项:

 -Djava.compiler=NONE 

希望优化发生,因为它将在现实生活中 – 如果JVM没有像您感兴趣的实际情况那样进行优化,那么测试将无效。

但是,如果你想确保JVM没有删除它可能会考虑no-ops的调用,否则一个选项就是使用结果 – 所以如果你反复调用System.currentTimeMillis() ,你可能会求和所有返回值,然后在结尾显示总和。

请注意,您可能仍然有一些偏见 – 例如,如果JVM可以廉价地确定自上次调用System.currentTimeMillis()以来只消耗了很少的时间,那么可能会有一些优化,因此它可以使用缓存值。 我并不是说这就是这种情况,但这是你需要考虑的事情。 最终,基准测试只能真正测试你给它们的负载。

还有一件事需要考虑:假设你想模拟一个代码运行很多的真实世界的情况,你应该在采取任何时间之前运行很多代码 – 因为Hotspot JVM将逐步更加优化,并且可能你关心的是高度优化的版本,不想测量JITting的时间和代码的“慢”版本。

正如斯蒂芬所提到的,你几乎肯定会把时间安排在循环之外 ……并且不要忘记实际使用结果……

对不起,但你想做的事情没什么意义。

如果关闭JIT编译,那么您只需要测量在关闭JIT编译的情况下调用该方法所需的时间。 这不是有用的信息……因为它告诉你几乎没有关于JIT编译打开时会发生什么的信息。

JIT开启和关闭之间的时间可能因巨大因素而不同。 在JIT关闭的情况下,你不太可能想要在生产中运行任何东西。

更好的方法是这样做:

 long before1 = getTimeInMilli(); for (int i = 0; i < TIMES_TO_ITERATE; i++) { doSomeReallyHardWork(); } long after1 = getTimeInMilli(); 

...和/或使用纳秒时钟。


如果您正在尝试测量调用两个版本的getTimeInMillis()所花费的时间,那么我不明白您对doSomeReallyHardWork()调用。 一个更明智的基准是:

 public long test() { long before1 = getTimeInMilli(); long sum = 0; for (int i = 0; i < TIMES_TO_ITERATE; i++) { sum += getTimeInMilli(); } long after1 = getTimeInMilli(); System.out.println("Took " + (after - before) + " milliseconds"); return sum; } 

......并多次打电话,直到印刷的时间稳定下来。

无论哪种方式,我的主要观点仍然存在,转向JIT编译和/或优化将意味着你正在测量一些无用的知识,而不是你真正想要找到的东西。 (除非,也就是说,你打算在关闭JIT的情况下运行你的应用程序......我觉得很难相信......)

您正在做什么看起来像基准测试,您可以阅读Robust Java基准测试,以获得有关如何使其正确的良好背景。 简而言之,您不需要将其关闭,因为它不会是生产服务器上发生的事情。相反,您需要知道关闭可能的“实时”估计/性能。 在优化之前,您需要“预热”您的代码,它看起来像:

 // warm up for (int j = 0; j < 1000; j++) { for (int i = 0; i < TIMES_TO_ITERATE; i++) { long before1 = getTimeInMilli(); doSomeReallyHardWork(); long after1 = getTimeInMilli(); } } // measure time long before = getTimeInMilli(); for (int j = 0; j < 1000; j++) { for (int i = 0; i < TIMES_TO_ITERATE; i++) { long before1 = getTimeInMilli(); doSomeReallyHardWork(); long after1 = getTimeInMilli(); } } long after = getTimeInMilli(); System.out.prinltn( "What to expect? " + (after - before)/1000 ); // average time 

当我们测量代码的性能时,我们使用这种方法,它使我们的代码需要更少的实时工作。 在分离方法中测量代码更好:

 public void doIt() { for (int i = 0; i < TIMES_TO_ITERATE; i++) { long before1 = getTimeInMilli(); doSomeReallyHardWork(); long after1 = getTimeInMilli(); } } // warm up for (int j = 0; j < 1000; j++) { doIt() } // measure time long before = getTimeInMilli(); for (int j = 0; j < 1000; j++) { doIt(); } long after = getTimeInMilli(); System.out.prinltn( "What to expect? " + (after - before)/1000 ); // average time 

第二种方法更精确,但它也取决于VM。 例如,HotSpot可以执行“堆栈内替换” ,这意味着如果方法的某些部分经常执行,它将由VM优化,旧版本的代码将在执行方法时与优化的代码交换。 当然,它需要VM端的额外操作。 JRockit没有这样做,只有在再次执行此方法时才会使用优化版本的代码(因此没有'运行时'优化...我的意思是在我的第一个代码示例中,所有旧代码都将被执行...除了doSomeReallyHardWork内部 - 它们不属于此方法,因此优化将很好地工作)。

更新:在我回答时编辑了有问题的代码;)