【问题标题】:Java - Repeated function call reduces execution timeJava - 重复的函数调用减少了执行时间
【发布时间】:2015-08-16 05:21:07
【问题描述】:

我有以下代码

public class BenchMark {
    public static void main(String args[]) {
        doLinear();

        doLinear();

        doLinear();

        doLinear();

    }


    private static void doParallel() {
        IntStream range = IntStream.range(1, 6).parallel();

        long startTime = System.nanoTime();
        int reduce = range
                .reduce((a, item) -> a * item).getAsInt();
        long endTime = System.nanoTime();
        System.out.println("parallel: " +reduce + " -- Time: " + (endTime - startTime));
    }

    private static void doLinear() {
        IntStream range = IntStream.range(1, 6);

        long startTime = System.nanoTime();
        int reduce = range
                .reduce((a, item) -> a * item).getAsInt();
        long endTime = System.nanoTime();
        System.out.println("linear: " +reduce + " -- Time: " + (endTime - startTime));
    }

}

我试图对流进行基准测试,但在一次又一次调用相同的函数时,执行时间稳步减少

输出:

linear: 120 -- Time: 57008226
linear: 120 -- Time: 23202
linear: 120 -- Time: 17192
linear: 120 -- Time: 17802

Process finished with exit code 0

第一次和第二次执行时间存在巨大差异

我确信 JVM 可能在幕后做了一些技巧,但谁能帮助我了解那里真正发生了什么?

有没有办法避免这种优化,以便我可以对真实的执行时间进行基准测试?

【问题讨论】:

  • 查看this question 讨论微基准测试
  • @greg-449 相当不错的基准测试指南。但是我仍然无法理解任何有关此优化的文档的原因。
  • 而且当我将范围增加到一个非常大的数字时,我看不到巨大的执行时间差异。似乎这发生在相当大的范围内。

标签: performance jvm java-8 java-stream


【解决方案1】:

我确信 JVM 可能会在幕后做一些技巧,但谁能帮助我了解那里真正发生了什么?

  1. 第一次调用的巨大延迟是由于整个 lambda 运行时子系统的初始化。您只需为整个申请支付一次费用。

  2. 当您的代码第一次到达任何给定的 lambda 表达式时,您需要为该 lambda 的链接付费(invokedynamic 调用站点的初始化)。

  3. 经过一些迭代后,您会看到由于 JIT 编译器优化了您的缩减代码,您会看到额外的加速。

有没有办法避免这种优化,以便我可以对真正的执行时间进行基准测试?

您在这里提出了一个矛盾:“真正的”执行时间是您在 预热后得到的时间,此时所有优化都已应用。这是实际应用程序会经历的运行时。前几次运行的延迟与更广泛的情况无关,除非您对单次性能感兴趣。

为了探索,您可以查看禁用 JIT 编译时代码的行为:将 -Xint 传递给 java 命令。还有更多标志会禁用优化的各个方面。

【讨论】:

  • 您不是总是需要编译代码才能运行它吗?如果我将我的项目构建成一个可运行的 jar,这会被视为“实际应用程序”吗?即使应用了所有优化,应用程序仍然需要支付你所说的“链接”成本,对吗?
  • 1.您必须编译为字节码,而不是机器码。 2.“实际应用程序”是您实际使用的,而不是一些脱离上下文的基准代码。 3. Lambda 链接发生在您的应用程序第一次执行包含 lambda 表达式的部分代码时。链接完成后应用 JIT 编译器优化。
【解决方案2】:

更新:请参阅 @Marko 的回答,了解由于 lambda 链接导致的初始延迟。


第一次调用的执行时间较长可能是JIT effect 的结果。简而言之,字节码到本地机器码的 JIT 编译发生在您的方法第一次被调用时。然后,JVM 通过识别经常调用的(热)方法来尝试进一步优化,并重新生成它们的代码以获得更高的性能。

有没有办法避免这种优化,以便我可以对真实的执行时间进行基准测试?

您当然可以通过排除前几个结果来解释 JVM 初始预热。然后在数万次迭代的循环中增加对您的方法的重复调用次数,并对结果进行平均。

post 中所讨论的,您可能还需要考虑将更多选项添加到您的执行中,以帮助减少噪音。这个post也有一些很好的提示。

【讨论】:

  • 当我将范围的上限从 6 增加到巨大的数字时,没有优化过程。似乎这发生在相当大的范围内。
  • 不,由于 lambda 链接(invokedynamic 调用站点的初始化)加上整个 LambdaMetaFactory 子系统的初始化,第一次运行速度较慢。后来的加速是由于 JITting。第一次输入的代码没有JIT编译。
  • 还要记住,JIT 编译是一个异步过程,因此不会导致方法调用的延迟。
  • @MarkoTopolnik 感谢您的反馈。我更新了我的答案以参考你的答案。
【解决方案3】:

真正的执行时间

没有像“真正的执行时间”这样的东西。如果您只需要解决此任务一次,则真正的执行时间将是第一次测试的时间(以及启动 JVM 本身的时间)。一般来说,执行给定代码所花费的时间取决于很多因素:

  • 这段代码是否被解释,由 C1 或 C2 编译器 JIT 编译。请注意,不只是三个选项。如果您从另一个调用一个方法,其中一个可能会被解释,而另一个可能会被 C2 编译。

  • 对于 C2 编译器:此代码以前是如何执行的,那么分支和类型配置文件中有什么。污染的类型配置文件会大大降低性能。

  • 垃圾收集器状态:是否中断执行

  • 编译队列:JIT-compiler是否同时编译其他代码(可能会减慢当前代码的执行速度)

  • 内存布局:对象如何在内存中定位,应该加载多少缓存行来访问所有必要的数据。

  • CPU 分支预测器状态取决于之前的代码执行情况,可能会增加或减少分支错误预测的数量。

等等等等。因此,即使您在孤立的基准测试中测量某些东西,这并不意味着生产中相同代码的速度会相同。它可能在数量级上有所不同。所以在测量之前你应该问自己为什么要测量这个东西。通常你并不关心程序的某些部分执行了多长时间。您通常关心的是整个程序的延迟和吞吐量。因此,分析整个程序并优化最慢的部分。可能您正在测量的东西不是最慢的。

【讨论】:

    【解决方案4】:

    Java VM 在第一次使用类时将类加载到内存中。 所以第一次和第二次运行的差异可能是类加载造成的。

    【讨论】:

      猜你喜欢
      • 2020-09-25
      • 1970-01-01
      • 1970-01-01
      • 2023-03-19
      • 2012-12-31
      • 2017-04-16
      • 2012-08-10
      • 1970-01-01
      • 2020-04-22
      相关资源
      最近更新 更多