【问题标题】:Minimizing Java function call overhead最小化 Java 函数调用开销
【发布时间】:2014-09-12 10:12:39
【问题描述】:

我有a piece of code,在我运行的每个测试中,函数调用都会产生大量开销。代码是一个紧密的循环,对数组的每个元素执行一个非常简单的函数(包含 4-8 百万ints)。

运行代码,主要包括

for (int y = 1; y < h; ++y) {
    for (int x = 1; x < w; ++x) {
        final int p = y * s + x;
        n[p] = f.apply(d, s, x, y);
    }
}

执行类似的操作

(final int[] d, final int s, final int x, final int y) -> {
    final int p = s * y + x;
    final int a = d[p] * 2
                + d[p - 1]
                + d[p + 1]
                + d[p - s]
                + d[p + s];
    return (1000 * (a + 500)) / 6000;
};

在各种机器上(我的工作笔记本电脑、带有 i7 3840QM 的 W530、带有 Xeon E5-1620 一个核心的服务器 VM,以及一个带有一个未知 CPU 核心的 Digital Ocean 节点),我反复得到统计显着性调用方法与内联时性能受到影响。所有测试均在 Java 1.8.0_11(Java HotSpot(TM) 64 位服务器虚拟机)上执行。

工作机器:

Benchmark                               Mode   Samples        Score  Score error    Units
c.s.q.ShaderBench.testProcessInline    thrpt       200       40.860        0.184    ops/s
c.s.q.ShaderBench.testProcessLambda    thrpt       200       22.603        0.159    ops/s
c.s.q.ShaderBench.testProcessProc      thrpt       200       22.792        0.117    ops/s

专用服务器,虚拟机:

Benchmark                               Mode   Samples        Score  Score error    Units
c.s.q.ShaderBench.testProcessInline    thrpt       200       40.685        0.224    ops/s
c.s.q.ShaderBench.testProcessLambda    thrpt       200       16.077        0.113    ops/s
c.s.q.ShaderBench.testProcessProc      thrpt       200       23.827        0.088    ops/s

做 VPS:

Benchmark                               Mode   Samples        Score  Score error    Units
c.s.q.ShaderBench.testProcessInline    thrpt       200       24.425        0.506    ops/s
c.s.q.ShaderBench.testProcessLambda    thrpt       200        9.643        0.140    ops/s
c.s.q.ShaderBench.testProcessProc      thrpt       200       13.733        0.134    ops/s

所有可接受的性能,但我有兴趣弄清楚为什么调用会有如此大的开销以及可以做些什么来优化它。目前正在试验不同的参数集。

内联所有潜在的操作会很困难,但理论上是可行的。将性能提高近 2 倍,这可能是值得的,但维护将是一场噩梦。

我不确定是否有合理的方法来批量处理一组重复;大多数操作需要多个输入(调用者不知道)并产生一个输出。

我还有哪些其他选项可以最大程度地减少开销和夜间性能?

【问题讨论】:

  • 仅供想回答的人参考,f 函数实际上是一个普通的接口方法,而不是@functionalInterface
  • @dkatzel 这是我的一个错误(这是一个使用 Java 8 的项目的一部分),尽管如果它以某种方式有帮助,可以保留一个。 f 可以由类或 lambda 填充,这一点相当重要。

标签: java performance optimization java-8 jmh


【解决方案1】:

方法调用不是问题,因为热方法通常是内联的。 虚拟调用是个问题。

在您的代码中,类型分析器被初始化方法Image.random 欺骗了。当Image.process第一次被JIT编译时,它被优化为调用random.nextInt()。因此,Image.process 的下一次调用将导致内联缓存未命中,然后是对 Shader.apply 的非优化虚拟调用。

  1. 从初始化方法中删除 Image.process 调用,然后 JIT 会将有用的调用内联到 Shader.apply

  2. 内联BlurShader.apply 后,您可以通过替换来帮助JIT 执行Common subexpression elimination 优化

    final int p = s * y + x;
    

    final int p = y * s + x;
    

    后面的表达式在Image.process中也遇到了,所以JIT不会计算同一个表达式两次。

应用这两个更改后,我已经达到了理想的基准分数:

Benchmark                           Mode   Samples         Mean   Mean error    Units
s.ShaderBench.testProcessInline    thrpt         5       36,483        1,255    ops/s
s.ShaderBench.testProcessLambda    thrpt         5       36,323        0,936    ops/s
s.ShaderBench.testProcessProc      thrpt         5       36,163        1,421    ops/s

【讨论】:

  • 之所以涉及到一个接口,而apply方法在另一个类中,是因为该部分是可插拔的,不能(手动)内联。对于示例/基准,只实现了一个过滤器。防止 Image.random 被内联听起来很有帮助。 JVM 是否可以保持多个版本的 Image.process 内联不同的函数?有什么标准或约定吗?
  • 对于一些额外的细节,希望有用,我计划有 4-6 个不同大小的层 (Images),每个层每秒处理一次或两次,每个使用不同的过滤器。我试图避免使 Image 抽象并编写 4-6 个子类。
  • @ssube HotSpot JVM 可以将最多 2 个最常用的接口实现内联到单个调用站点。将有其他实现的虚拟方法调用。这只是特定 JVM 的一种行为,没有包含在任何约定中。要查看实际的 JIT 内联活动,请使用以下 JVM 选项:-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
  • 如果您发现真实场景的性能不理想,我建议为每个过滤器创建单独的process 方法,以便这些方法可以处理整个图像,而不仅仅是一个像素。
  • 这似乎是唯一可行的选择。我必须对接口进行一些重构以保持干净,但它可能会工作得很好。
猜你喜欢
  • 2015-05-22
  • 2017-01-11
  • 2016-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-08
  • 1970-01-01
  • 2012-03-15
相关资源
最近更新 更多