Megamorphic 虚拟调用 - 尝试对其进行优化

Megamorphic virtual call - trying to optimize it

我们知道有一些技术可以使 JVM 中的虚拟调用不那么昂贵,例如内联缓存或多态内联缓存。

让我们考虑以下情况:

Base是一个接口。

public void f(Base[] b) {
    for(int i = 0; i < b.length; i++) {
          b[i].m();   
    }
}

我从我的探查器中看到调用虚拟(接口)方法 m 相对昂贵。 f 在热路径上,它被编译为机器代码 (C2),但我看到对 m 的调用是一个真正的虚拟调用。这意味着它没有被JVM优化。

问题是,遇到这种情况怎么办?显然,我不能在这里使方法 m 不是虚拟的,因为它需要认真的重新设计。

我能做点什么还是我必须接受?我在想如何“强制”或“说服”JVM

  1. 在这里使用多态内联缓存 - b` 中不同类型的数量非常少 - 在 4-5 种类型之间。
  2. 展开这个循环 - b 的长度也相对较小。展开后,内联缓存可能会在这里有所帮助。

在此先感谢您的任何建议。 此致,

HotSpot JVM 最多可以内联一个虚拟调用的两个不同目标,对于更多接收者,将通过 vtable/itable [1].

调用

要强制内联更多接收器,您可以尝试手动将调用去虚拟化,例如

if (b.getClass() == X.class) {
    ((X) b).m();
} else if (b.getClass() == Y.class) {
    ((Y) b).m();
} ...

在分析代码执行期间(在解释器或 C1 中),JVM 收集每个调用站点的接收器类型统计信息。然后在优化编译器 (C2) 中使用此统计信息。您的示例中只有一个调用站点,因此统计信息将在整个执行过程中汇总。

但是,例如,如果 b[0] 总是只有两个接收者 XY,而 b[1] 总是有另外两个接收者 ZW,JIT 编译器可能受益于将代码拆分为多个调用站点,即手动展开:

int len = b.length;
if (len > 0) b[0].m();
if (len > 1) b[1].m();
if (len > 2) b[2].m();
...

这将拆分类型配置文件,以便 b[0].m()b[1].m() 可以单独优化。

这些是依赖于特定 JVM 实现的低级技巧。一般来说,我不会推荐它们用于生产代码,因为这些优化很脆弱,但它们肯定会使源代码更难阅读。毕竟,巨态调用并没有那么糟糕 [2].

[1] https://shipilev.net/blog/2015/black-magic-method-dispatch/
[2] https://shipilev.net/jvm/anatomy-quarks/16-megamorphic-virtual-calls/