【问题标题】:Trying to benchmark lambda performance尝试对 lambda 性能进行基准测试
【发布时间】:2016-01-15 18:47:51
【问题描述】:

我已经阅读了这篇文章:Performance difference between Java 8 lambdas and anonymous inner classes 并在那里提供了article

上面写着:

Lambda 调用的行为与匿名类调用完全相同

“好的”我说完决定自己写基准,我用过jmh,下面是(我还添加了基准供方法参考)。

public class MyBenchmark {

    public static final int TESTS_COUNT = 100_000_000;

    @Benchmark
    public void testMethod_lambda() {
        X x = i -> test(i);
        for (long i = 0; i < TESTS_COUNT; i++) {
            x.x(i);
        }
    }
    @Benchmark
    public void testMethod_methodRefernce() {
        X x = this::test;
        for (long i = 0; i < TESTS_COUNT; i++) {
            x.x(i);
        }
    }
    @Benchmark
    public void testMethod_anonymous() {
        X x = new X() {
            @Override
            public void x(Long i) {
                test(i);
            }
        };
        for (long i = 0; i < TESTS_COUNT; i++) {
            x.x(i);
        }
    }

    interface X {
        void x(Long i);
    }

    public void test(Long i) {
        if (i == null) System.out.println("never");
    }
}

结果(在 Intel Core i7 4770k 上)是:

Benchmark                                     Mode  Samples   Score  Score error  Units
t.j.MyBenchmark.testMethod_anonymous         thrpt      200  16,160        0,044  ops/s
t.j.MyBenchmark.testMethod_lambda            thrpt      200   4,102        0,029  ops/s
t.j.MyBenchmark.testMethod_methodRefernce    thrpt      200   4,149        0,022  ops/s

因此,正如您所见,lambda 和匿名方法调用之间存在 4 倍的差异,其中 lambda 的速度要慢 4 倍。

问题是:我做错了什么或者我对 lambdas 的性能理论有误解?

编辑

# VM invoker: C:\Program Files\Java\jre1.8.0_31\bin\java.exe
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each

【问题讨论】:

  • 查看生成的字节码,我注意到匿名类使用invokespecial 构造对象,而lambda 使用invokedynamic。如果您在测试本身之外构造每个对象的一个​​实例,我很想知道是否存在任何速度差异。
  • 为什么要在 JMH 测试中运行循环? JMH 为您执行循环。为什么你使用那个 "never" 东西而不是黑洞?编译器足够聪明,可以优化它。
  • 此外,您引用的“Lambda 调用的行为与匿名类调用”完全一样。您方便地省略了脚注。
  • 请注意,如果您将 lambda、方法引用和匿名类的创建删除到私有静态字段(并重用它们),您的基准测试表明方法引用和匿名类具有相同的性能,而 lambda性能较差。
  • "我做错了什么?" - 1) 手动编写基准循环是错误的; JMH 为您服务。 2) 结果不被 Blackhole 消耗 => JVM 可能会奇怪地优化代码,而你的测量结果不是你所期望的。

标签: java performance lambda java-8 microbenchmark


【解决方案1】:

问题在于您的基准测试:您是死代码消除的受害者。

JIT 编译器很聪明地理解有时自动装箱的结果永远不会为空,因此对于匿名类,它只是删除了您的检查,这反过来又使循环体几乎为空。用不那么明显的东西(对于 JIT)替换它:

public void test(Long i) {
    if (i == Long.MAX_VALUE) System.out.println("never");
}

您将观察到相同的性能(匿名类变得更慢,而 lambda 和方法引用在同一级别执行)。

对于 lambda/方法参考,由于某种原因,它没有进行相同的优化。但是您不必担心:在实际代码中您不太可能拥有可以完全优化的这种方法。

一般来说@apangin 是对的:改用 Blackhole。

【讨论】:

  • 是的,我刚刚测试过,这确实是问题所在。一般来说@Andremoniy,你不应该尝试在你的基准测试中变得聪明,JIT 或者编译器会更聪明。
  • @TagirValeev 是的,看起来我真的弄错了。非常感谢您的课程
  • 我真的很惊讶i == Long.MAX_VALUE 是“不太明显”的东西,足以欺骗 JVM。毕竟,我们有一个完全可预测的循环计数器和一个装箱,这是 JVM 已知的操作……
【解决方案2】:

除了@TagirValeev 提出的问题之外,您采用的基准方法存在根本缺陷,因为您正在衡量一个复合指标(尽管您尝试不这样做。)

您要独立衡量的重要成本是链接捕获调用。但是你所有的测试都会把每一种测试都涂抹在一起,从而毒害你的结果。我的建议是只关注调用成本——这与整体应用程序吞吐量最相关,也是最容易衡量的(因为它受多个级别缓存的影响较小。)

底线:在动态编译环境中测量性能确实非常困难。即使是 JMH。

【讨论】:

    【解决方案3】:

    我的问题是您不应该如何进行基准测试的另一个例子。我已经根据此处其他答案中的建议重新创建了我的测试。

    希望现在它接近正确性,因为它表明 lambda 和 anon 的方法调用性能之间没有任何显着差异。见下文:

    @State(Scope.Benchmark)
    public class MyBenchmark {
    
        @Param({"1", "100000", "500000"})
        public int arg;
    
        @Benchmark
        public void testMethod_lambda(Blackhole bh) {
            X x = (i, bh2) -> test(i, bh2);
            x.x(arg, bh);
        }
    
        @Benchmark
        public void testMethod_methodRefernce(Blackhole bh) {
            X x = this::test;
            x.x(arg, bh);
        }
    
        @Benchmark
        public void testMethod_anonymous(Blackhole bh) {
            X x = new X() {
                @Override
                public void x(Integer i, Blackhole bh) {
                    test(i, bh);
                }
            };
            x.x(arg, bh);
        }
    
        interface X {
            void x(Integer i, Blackhole bh);
        }
    
        public void test(Integer i, Blackhole bh) {
            bh.consume(i);
        }
    }
    Benchmark                                     (arg)   Mode  Samples          Score  Score error  Units
    t.j.MyBenchmark.testMethod_anonymous              1  thrpt      200  415893575,928  1353627,574  ops/s
    t.j.MyBenchmark.testMethod_anonymous         100000  thrpt      200  394989882,972  1429490,555  ops/s
    t.j.MyBenchmark.testMethod_anonymous         500000  thrpt      200  395707755,557  1325623,340  ops/s
    t.j.MyBenchmark.testMethod_lambda                 1  thrpt      200  418597958,944  1098137,844  ops/s
    t.j.MyBenchmark.testMethod_lambda            100000  thrpt      200  394672254,859  1593253,378  ops/s
    t.j.MyBenchmark.testMethod_lambda            500000  thrpt      200  394407399,819  1373366,572  ops/s
    t.j.MyBenchmark.testMethod_methodRefernce         1  thrpt      200  417249323,668  1140804,969  ops/s
    t.j.MyBenchmark.testMethod_methodRefernce    100000  thrpt      200  396783159,253  1458935,363  ops/s
    t.j.MyBenchmark.testMethod_methodRefernce    500000  thrpt      200  395098696,491  1682126,737  ops/s
    

    【讨论】:

    • 我认为您仍然希望将 IC/lambda 的创建分解为静态代码。否则,您仍然会使用链接/捕获成本来毒害一些调用测量。
    • @BrianGoetz 好吧,我只是想表明我之前的结论是错误的,并且正确的基准测试表明调用性能没有任何差异。在这种情况下我错在哪里?
    • 您正在衡量两个具有独立性能配置文件的不同操作的成本总和。就像衡量“去商店买牛奶”和“坐在餐桌旁喝一杯牛奶”的成本。因为,你常见的操作不是“买牛奶+喝牛奶”,而是“喝牛奶”。碰巧你必须先买一些才能喝。但很明显,牛奶购买受到很多与喝牛奶无关的因素(一天中的时间、交通模式、商店营业时间等)的影响。你想分别测量每一个。
    • 我想你还是没明白。链接(a)比调用要昂贵得多,(b)只做一次,(c)受到许多因素的扭曲。如果您认为常见的情况是链接+调用一次,那么几乎按照定义,您并不关心性能! (内部类的链接涉及到文件系统,从磁盘读取类文件字节,以及解析/验证/加载类。)就像说常见的操作是“建造建筑物,然后乘电梯到10楼。 "让性能分析变得困难的部分原因是知道要衡量什么
    • 您仍然可以让test 成为实例方法;静态捕获 X 的实例,并在您的 @Bench 方法中测量 x.x(..)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-05-29
    • 2014-07-30
    • 2012-02-02
    • 1970-01-01
    • 2012-03-28
    • 2016-12-25
    • 2014-12-19
    相关资源
    最近更新 更多