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
- 在这里使用多态内联缓存 - b` 中不同类型的数量非常少 - 在 4-5 种类型之间。
- 展开这个循环 -
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]
总是只有两个接收者 X
或 Y
,而 b[1]
总是有另外两个接收者 Z
或W
,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/
我们知道有一些技术可以使 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
- 在这里使用多态内联缓存 - b` 中不同类型的数量非常少 - 在 4-5 种类型之间。
- 展开这个循环 -
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]
总是只有两个接收者 X
或 Y
,而 b[1]
总是有另外两个接收者 Z
或W
,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/