Java如何通过即时编译提高低效代码的运行速度?

9
在下面的代码片段中,Foo1是一个类,每次调用方法bar()时都会增加计数器。Foo2也是这样做的,但多了一层间接性。
我本来期望Foo1Foo2快,但实际上,Foo2始终比Foo1快40%。JVM如何优化代码,使得Foo2运行比Foo1更快呢?
一些细节:
  • 测试是使用java -server CompositionTest执行的。
  • 使用java -client CompositionTest运行测试会产生预期结果,即Foo2Foo1慢。
  • 改变循环的顺序没有任何影响。
  • 在sun和openjdk的JVM上验证了结果。
代码:
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);
    }
}

7
你尝试交换循环,看是否会有差异了吗? - AHungerArtist
8
阅读一篇关于Java微基准测试的好的Stack Overflow帖子可能是个好主意,然后回顾你的代码以确保你的测试结果确实有意义。 - QuantumMechanic
我尝试了(使用JRE7-server),但没有任何区别。什么?!我知道它应该有所改善,因为JRE需要预热和编译代码,但是……完全没有任何区别。 - Petr Janeček
1
哇,好问题。我发现如果你总计所花费的时间(而不仅仅是测量最后循环的计数),那么这两种方法的差距就会变得更小(尽管奇怪的是,foo2 的速度要快约 2%)。至于原因,? - Chris
@AHungerArtist:交换循环的顺序不会有任何影响。 - TianyuZhu
@Chris:只看最后一次测量可以让JVM“热身”代码。代码的前几次运行是解释执行的,而后面的运行将被编译。 - TianyuZhu
2个回答

2
你的微基准测试没有意义。在我的电脑上,每个循环的代码运行时间约为8毫秒……要得出任何有意义的数字,基准测试可能应该运行至少一秒钟。
当两者都运行了大约一秒钟(提示:您需要超过Integer.MAX_VALUE次重复),我发现两者的运行时间是相同的。
这样做的可能解释是JIT编译器已经注意到你的间接寻址是无意义的,并将其优化掉(或者至少内联了方法调用),使得两个循环中执行的代码是相同的。
它可以这样做,因为它知道Foo2中的bar实际上是final的,它也知道传递给Foo2构造函数的参数始终是一个Foo1(至少在我们的小测试中)。这样它就知道了当调用Foo2.bar时的确切代码路径。它还知道这个循环将会运行很多次(事实上它知道循环将执行多少次)——所以内联代码似乎是一个好主意。
我不知道它是否精确地这样做,但这些都是JIT可能对代码进行的逻辑观察。也许在将来,一些JIT编译器甚至可能优化整个while循环,只需将计数设置为reps,但这似乎不太可能。

你的电脑应该非常快。我建议你将“reps”从“int”更改为“long”,并使用“Long.MAX_VALUE” reps 重试。 - TianyuZhu
我有一台i3电脑... 我已将您的代码转换为C++,当使用“Integer.MAX_VALUE”时,执行时间要长得多(“Foo1”需要13秒,“Foo2”需要22秒)。对于Java版本,如果我只是简单地将“i”的类型更改为long(不是值,只是类型),那么执行时间现在约为3秒。正如其他人所说,微基准测试在Java上从来都不是一个好主意。 - Dunes

1

试图预测现代语言的性能并不是很有效。

JVM 不断地进行修改,以提高常见、可读的结构的性能,相反,使不常见、笨拙的代码变慢。

只需尽可能清晰地编写代码--然后,如果您真正确定代码实际上被识别为太慢而无法通过书面规范,您可能需要手动调整某些区域--但这可能涉及大型、简单的想法,如对象缓存、调整 JVM 选项和消除真正愚蠢/错误的代码(错误的数据结构可能非常巨大,我曾经将 ArrayList 更改为 LinkedList,并将一个操作从 10 分钟减少到 5 秒,将发现类 B 网络的 ping 操作多线程化,将一个操作从 8+ 小时缩短到几分钟)。


1
我只是想了解JVM的行为。在这种情况下,我只是好奇JVM如何处理如此多级别的间接引用。这不是“过早优化”。我只是想理解我编写的代码是如何被JIT编译器解释的。 - TianyuZhu
@TianyuZhu 我完全理解,我想我可以说JVM的行为是变化和适应的,因此试图使用您当前对JVM的知识来提高速度是一件坏事,但了解它总是有趣的。 - Bill K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接