JVM常用概念之超态虚拟调用

server/2025/3/18 3:18:34/

问题

超态虚拟调用是什么?

基础知识

大部分认为超态调用是非常糟糕的,主要是因为超态调用会调用慢路径,并且无法享受编译器优化,那OpenJDK可以取消超态调用吗?那在发生超态调用时我们可以做什么呢?

实验

源码

import org.openjdk.jmh.annotations.*;@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class VirtualCall {static abstract class A {int c1, c2, c3;public abstract void m();}static class C1 extends A {public void m() { c1++; }}static class C2 extends A {public void m() { c2++; }}static class C3 extends A {public void m() { c3++; }}A[] as;@Param({"mono", "mega"})private String mode;@Setuppublic void setup() {as = new A[300];boolean mega = mode.equals("mega");for (int c = 0; c < 300; c += 3) {as[c]   = new C1();as[c+1] = mega ? new C2() : new C1();as[c+2] = mega ? new C3() : new C1();}}@Benchmarkpublic void test() {for (A a : as) {a.m();}}
}

运行结果

通过-XX:LoopUnrollLimit=1 -XX:-TieredCompilation来运行上述用例,该运行参数的主要作用是阻止循环展开使反汇编复杂化,而禁用分层编译将保证使用最终优化编译器进行编译。执行结果如下:

Benchmark         (mode)  Mode  Cnt     Score    Error  Units
VirtualCall.test    mono  avgt    5   325.478 ± 18.156  ns/op
VirtualCall.test    mega  avgt    5  1070.304 ± 53.910  ns/op

通过-XX:CompileCommand=exclude,org.openjdk.VirtualCall::test来模拟不使用优化编译器的运行,其执行结果如下:

Benchmark         (mode)  Mode  Cnt      Score     Error  Units
VirtualCall.test    mono  avgt    5  11598.390 ± 535.593  ns/op
VirtualCall.test    mega  avgt    5  11787.686 ± 884.384  ns/op

通过上述的运行结果可知,超态调用确实会花费一些成本,但绝对不会对性能造成影响。优化情况下的“mono”和“mega”之间的区别基本上在于调用开销:对于“mega”情况,我们花费每个元素 3ns,而对于“mono”情况,我们仅花费每个元素 1ns。

通过通过-prof perfasm对“mega” 案例进行分析,执行结果如下:

....[Hottest Region 1].......................................................................
C2, org.openjdk.generated.VirtualCall_test_jmhTest::test_avgt_jmhStub, version 88 (143 bytes)6.93%    5.40%  ↗  0x...5c450: mov    0x40(%rsp),%r9│  ...3.65%    4.31%  │  0x...5c47b: callq  0x...0bf60 ;*invokevirtual m│                            ; - org.openjdk.VirtualCall::test@22 (line 76);   {virtual_call}3.12%    2.34%  │  0x...5c480: inc    %ebp3.33%    0.02%  │  0x...5c482: cmp    0x10(%rsp),%ebp╰  0x...5c486: jl     0x...5c450...
.............................................................................................31.26%   21.77%  <total for region 1>....[Hottest Region 2].......................................................................
C2, org.openjdk.VirtualCall$C1::m, version 84 (14 bytes) <--- mis-attributed :(...Decoding VtableStub vtbl[5]@123.95%    1.57%     0x...59bf0: mov    0x8(%rsi),%eax3.73%    3.34%     0x...59bf3: shl    $0x3,%rax3.73%    5.04%     0x...59bf7: mov    0x1d0(%rax),%rbx16.45%   22.42%     0x...59bfe: jmpq   *0x40(%rbx)        ; jump to target0x...59c01: add    %al,(%rax)0x...59c03: add    %al,(%rax)...
.............................................................................................27.87%   32.37%  <total for region 2>....[Hottest Region 3].......................................................................
C2, org.openjdk.VirtualCall$C3::m, version 86 (26 bytes)# {method} {0x00007f75aaf4dd50} 'm' '()V' in 'org/openjdk/VirtualCall$C3'...[Verified Entry Point]17.82%   26.04%    0x...595c0: sub    $0x18,%rsp0.06%    0.04%    0x...595c7: mov    %rbp,0x10(%rsp)0x...595cc: incl   0x14(%rsi)       ; c3++3.53%    5.14%    0x...595cf: add    $0x10,%rsp0x...595d3: pop    %rbp3.29%    5.10%    0x...595d4: test   %eax,0x9f01a26(%rip)0.02%    0.02%    0x...595da: retq...
.............................................................................................24.73%   36.35%  <total for region 3>

因此,基准测试循环会调用某个函数(我们可以假设它是虚拟调用处理程序),然后它以 VirtualStub 结束,VirtualStub 据称会执行其他所有运行时对虚拟调用所执行的操作:在虚拟方法表 (VMT)的帮助下跳转到实际方法。

反汇编表明我们实际上是在调用0x…​0bf60 ,而不是调用位于0x…​59bf0的VirtualStub ?!而且该调用很频繁,所以调用目标也应该很频繁,对吧?这就是运行时本身对我们耍花招的地方。即使编译器放弃优化虚拟调用,运行时也可以自行处理“意外”情况。为了更好地诊断这一点,我们需要获取fastdebug OpenJDK 构建,并为内联缓存 (IC)提供跟踪选项: -XX:+TraceIC 。此外,我们希望使用-prof perfasm:saveLog=true将热点日志保存到文件中,在该日志文件中检索可知:

$ grep IC org.openjdk.VirtualCall.test-AverageTime.logIC@0x00007fac4fcb428b: to megamorphic {method} {0x00007fabefa81880} 'm' ()V';in 'org/openjdk/VirtualCall$C2'; entry: 0x00007fac4fcb2ab0

检索结果表明内联缓存已为0x00007fac4fcb428b处的调用点执行了操作。这里的调用就是java调用。

$ grep -A 4 0x00007fac4fcb428b: org.openjdk.VirtualCall.test-AverageTime.log0.02%    0x00007fac4fcb428b: callq  0x00007fac4fb7dda0;*invokevirtual m {reexecute=0 rethrow=0 return_oop=0}; - org.openjdk.VirtualCall::test@22 (line 76);   {virtual_call}

但是 Java 调用中的地址是什么?这是解析运行时存根:

$ grep -C 2  0x00007fac4fb7dda0 org.openjdk.VirtualCall.test-AverageTime.log0x00007fac4fb7dcdf: hltDecoding RuntimeStub - resolve_virtual_call 0x00007fac4fb7dd100x00007fac4fb7dda0: push   %rbp0x00007fac4fb7dda1: mov    %rsp,%rbp0x00007fac4fb7dda4: pushfq

其实java调用调用了运行时,确认了需要调用的方法,然后要求 IC修补调用以指向新的解析地址!由于这是一次性操作,这就是为什么没有将其视为热代码。IC 操作行提到将条目更改为另一个地址,顺便说一下,这是我们的实际 VtableStub:

$ grep -C 4 0x00007fac4fcb2ab0: org.openjdk.VirtualCall.test-AverageTime.logDecoding VtableStub vtbl[5]@128.94%    6.49%    0x00007fac4fcb2ab0: mov    0x8(%rsi),%eax0.16%    0.06%    0x00007fac4fcb2ab3: shl    $0x3,%rax0.20%    0.10%    0x00007fac4fcb2ab7: mov    0x1e0(%rax),%rbx2.34%    1.90%    0x00007fac4fcb2abe: jmpq   *0x40(%rbx)0x00007fac4fcb2ac1: int3

最后,无需运行时/编译器调用即可调度已解析的调用:调用站点只需调用执行 VMT 调度的VtableStub — 永远不会离开生成的机器代码。此 IC 机制将以类似的方式处理虚拟单态和静态调用,指向不执行 VMT 调度的存根/地址。

我们可以在初始 JMH perfasm 输出中看到的是编译后、执行和潜在运行时修补之前的生成代码。

总结

仅仅因为编译器未能针对最佳情况进行优化,并不意味着最坏情况会更糟糕。确实,你会放弃一些优化,但开销不会大到需要完全避免虚拟调用。

文章来源:https://blog.csdn.net/nanxiaotao/article/details/146282499
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ppmy.cn/server/175587.html

相关文章

Deepseek -> 如何在PyTorch中合并张量

嗯&#xff0c;用户问的是如何在PyTorch中合并张量。我得先回忆一下PyTorch里有哪些常用的拼接函数。记得常用的有torch.cat和torch.stack&#xff0c;可能还有别的比如torch.hstack、vstack之类的。那这两个主要的有什么区别呢&#xff1f; 首先&#xff0c;torch.cat是在已有…

leetCode求两数之和(第一题)

一、题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回答…

linux 命令 tree

Linux 的 tree 命令以树状结构显示目录及其子目录内容&#xff0c;非常适合直观查看文件层级。以下是其核心用法和常见示例&#xff1a; 安装 tree # CentOS/RHEL sudo yum install tree 基本语法 tree [选项] [目录路径] 常用选项 1. 控制显示层级 -L [数字]&#xff1a;限…

k8s系统学习路径

学习 Kubernetes&#xff08;K8s&#xff09;需要循序渐进&#xff0c;结合理论知识和实践操作。以下是学习 Kubernetes 的推荐步骤&#xff1a; 1. 先决条件 • 掌握容器基础&#xff1a;先学习 Docker&#xff0c;理解容器化概念&#xff08;镜像、容器、仓库&#xff09;、…

TCP/IP原理详细解析

前言 TCP/IP是一种面向连接&#xff0c;可靠的传输&#xff0c;传输数据大小无限制的。通常情况下&#xff0c;系统与系统之间的http连接需要三次握手和四次挥手&#xff0c;这个执行过程会产生等待时间。这方面在日常开发时需要注意一下。 TCP/IP 是互联网的核心协议族&…

【spring-boot-starter-data-neo4j】创建结点和查找结点操作

配置连接neo4j # application.properties spring.neo4j.uribolt://localhost:7687 spring.neo4j.authentication.usernameneo4j spring.neo4j.authentication.password你的密码定义实体类 package com.anmory.platform.GraphService.Dao;import org.springframework.data.neo…

rust 的Clone

Clone 是 Rust 编程语言中一个核心特质&#xff08;trait&#xff09;&#xff0c; 定义了类型如何安全、明确地创建其值的深拷贝&#xff08;deep copy&#xff09;。 下面用实例来演示Clone的作用&#xff0c;先看一下如下的代码&#xff0c;注意此代码编译不过。 #[derive…

C#类型转换大总结

在 C# 中,类型转换是将数据从一种类型转换为另一种类型的过程,常见的转换方式包括隐式转换、显式转换、方法转换(如 Convert 类或 Parse/TryParse)以及自定义转换操作符。以下是详细的分类和示例: 隐式转换(Implicit Conversion) 无需显式声明,编译器自动完成,通常发生…