在以下代码段中,Foo1
是一个每次调用方法bar()
时递增计数器的类。 Foo2
做同样的事情,但有一个额外的间接层。
我希望Foo1
比Foo2
更快,但在实践中,Foo2
始终比Foo1
快40%。 JVM如何优化代码,使Foo2
的运行速度超过Foo1
?
java -server CompositionTest
执行。java -client CompositionTest
运行测试会产生预期结果,Foo2
慢于Foo1
。public class CompositionTest {
private static interface DoesBar {
public void bar();
public int count();
public void count(int c);
}
private static final class Foo1 implements DoesBar {
private int count = 0;
public final void bar() { ++count; }
public int count() { return count; }
public void count(int c) { count = c; }
}
private static final class Foo2 implements DoesBar {
private DoesBar bar;
public Foo2(DoesBar bar) { this.bar = bar; }
public final void bar() { bar.bar(); }
public int count() { return bar.count(); }
public void count(int c) { bar.count(c); }
}
public static void main(String[] args) {
long time = 0;
DoesBar bar = null;
int reps = 100000000;
for (int loop = 0; loop < 10; loop++) {
bar = new Foo1();
bar.count(0);
int i = reps;
time = System.nanoTime();
while (i-- > 0) bar.bar();
time = System.nanoTime() - time;
if (reps != bar.count())
throw new Error("reps != bar.count()");
}
System.out.println("Foo1 time: " + time);
for (int loop = 0; loop < 10; loop++) {
bar = new Foo2(new Foo1());
bar.count(0);
int i = reps;
time = System.nanoTime();
while (i-- > 0) bar.bar();
time = System.nanoTime() - time;
if (reps != bar.count())
throw new Error("reps != bar.count()");
}
System.out.println("Foo2 time: " + time);
}
}
答案 0 :(得分:2)
你的microbench标记毫无意义。在我的计算机上,每个循环的代码运行大约8ms ...要获得任何有意义的数字,基准测试应该至少运行一秒钟。
当两者都运行大约一秒钟(提示,你需要超过Integer.MAX_VALUE
次重复)时,我发现两者的运行时间是相同的。
可能的解释是,JIT编译器注意到你的间接是没有意义的并且优化了它(或者至少内联了方法调用),这样两个循环中执行的代码是相同的。
它可以这样做,因为它知道bar
中的Foo2
实际上是最终的,它也知道Foo2
构造函数的参数始终是Foo1
(至少在我们的小测试中)。这样,它就知道调用Foo2.bar
时的确切代码路径。它也知道这个循环会运行很多次(实际上它确切地知道循环执行的次数) - 所以内联代码似乎是一个好主意。
我不知道这是否正是它的作用,但这些都是JIT可以对代码做出的逻辑观察。也许在未来,一些JIT编译器甚至可能优化整个while循环并简单地将计数设置为reps,但这似乎不太可能。
答案 1 :(得分:1)
尝试预测现代语言的表现效率不高。
JVM经常被修改,以提高常见的可读结构的性能,相反,它会使不常见的,笨拙的代码变慢。
尽可能清楚地编写代码 - 然后如果您确实认为代码实际上被识别为太慢而无法通过书面规范,那么您可能需要手动调整某些区域 - 但这可能会涉及大而简单的想法,如对象缓存,调整JVM选项和消除真正愚蠢/错误的代码(错误的数据结构可能是巨大的,我曾经将ArrayList更改为LinkedList并将操作从10分钟减少到5秒,多线程发现B类网络的ping操作从8小时到几分钟进行了操作。