【问题标题】:How is Java jitting inefficient code to run faster than efficient code?Java 如何让低效代码比高效代码运行得更快?
【发布时间】:2012-05-25 13:43:59
【问题描述】:

在下面的代码 sn-p 中,Foo1 是一个类,每次调用方法 bar() 时都会增加一个计数器。 Foo2 做同样的事情,但多了一层间接性。

我希望 Foo1Foo2 快,但实际上,Foo2 始终比 Foo1 快 40%。 JVM 是如何优化代码使Foo2 运行速度比Foo1 快的?

一些细节

  • 使用java -server CompositionTest 执行测试。
  • 使用java -client CompositionTest 运行测试会产生预期结果,即Foo2Foo1 慢。
  • 切换循环的顺序没有区别。
  • 在 sun 和 openjdk 的 JVM 上使用 java6 验证了结果。

代码

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);
    }
}

【问题讨论】:

  • 您是否尝试切换循环以查看是否有所作为?
  • 阅读一篇关于 Java microbenchmarking 的优秀 SO 帖子可能是个好主意,然后检查您的代码以确保您的结果确实有意义。
  • 我试过了(使用 JRE7 -server),但没有任何区别。什么?!我知道它应该,JRE 需要预热和编译代码,但是......它没有任何区别。完全没有。
  • 哇。好问题。我发现,如果您将总体花费的时间加起来(而不仅仅是测量最终循环的计数),那么这两种方法会更接近(尽管奇怪的是 foo2 快了大约 2%)。至于原因呢?
  • @AHungerArtist:切换循环的顺序并没有什么不同。

标签: java performance optimization jvm jit


【解决方案1】:

您的微基准测试毫无意义。在我的计算机上,每个循环的代码运行时间约为 8 毫秒……要获得任何有意义的数字,基准测试可能至少应该运行一秒钟。

当两者都运行大约一秒钟时(提示,您需要超过 Integer.MAX_VALUE 重复次数)我发现两者的运行时间是相同的。

对此的可能解释是,JIT 编译器注意到您的间接寻址毫无意义并对其进行了优化(或至少内联方法调用),以便在两个循环中执行的代码相同。

它可以这样做是因为它知道Foo2 中的bar 实际上是最终的,它还知道Foo2 构造函数的参数始终是Foo1(至少在我们的小测试中) .这样它就知道调用Foo2.bar 时的确切代码路径。它还知道这个循环会运行很多次(实际上它知道循环将执行多少次)——因此内联代码似乎是个好主意。

我不知道这是否正是它所做的,但这些都是 JIT 可以让我对代码进行的合乎逻辑的观察。也许在未来,一些 JIT 编译器甚至可能会优化整个 while 循环并简单地将 count 设置为 reps,但这似乎不太可能。

【讨论】:

  • 你的电脑一定很快。我建议您将 repsint 更改为 long 并使用 Long.MAX_VALUE reps 重试。
  • 我有一个 i3 ...使用Integer.MAX_VALUE。对于 java 版本,如果我做一些简单的事情,比如将 i 的类型更改为 long(不是值,只是类型),执行时间现在大约是 3 秒。正如其他人所说,在 Java 上进行微基准测试从来都不是一个好主意。
【解决方案2】:

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

不断修改 JVM 以提高常见、可读结构的性能,相比之下,这会使不常见、笨拙的代码变慢。

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

【讨论】:

  • 我只是想了解 JVM 的行为。在这种情况下,我只是对 JVM 如何处理这种间接级别感到好奇。这不是过早的优化。我只是想了解 jit 编译器如何解释我编写的代码。
  • @TianyuZhu 我完全理解,我想我会说 JVM 的行为是改变和适应——所以试图利用你目前对 JVM 的了解来提高速度是不好的东西——但学习它总是很有趣。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-31
  • 2017-08-28
  • 2017-07-11
  • 2021-11-03
  • 1970-01-01
  • 2013-06-20
相关资源
最近更新 更多