关于java:如何确保不会发生jvm和编译器优化

how to make sure no jvm and compiler optimization occurs

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

1
2
3
4
5
6
7
8
9
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。添加到您的运行命令下一个选项:

1
-Djava.compiler=NONE


您希望优化发生,因为它将在现实生活中 - 如果JVM没有以与您感兴趣的实际情况相同的方式进行优化,则测试将无效。

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

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

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

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


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

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

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

更好的方法是这样做:

1
2
3
4
5
long before1 = getTimeInMilli();
for (int i = 0; i < TIMES_TO_ITERATE; i++) {
    doSomeReallyHardWork();
}
long after1 = getTimeInMilli();

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

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

1
2
3
4
5
6
7
8
9
10
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基准测试,以获得有关如何使其正确的良好背景。简而言之,您不需要将其关闭,因为它不会是生产服务器上发生的事情。相反,您需要知道关闭可能的"实时"估计/性能。在优化之前,您需要"预热"您的代码,它看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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内部 - 它们不属于此方法,因此优化将很有效。

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