当前位置: 首页 > article >正文

JVM常用概念之编译器黑洞

问题

JMH 如何避免微小基准测试中的不会运行的代码的消除工作?是否有隐式或显式编译器支持?

基础知识

优化编译器擅长优化简单的东西。例如,如果存在任何人都无法观察到的计算,则可以将其视为“不会运行的代码”并将其删除。

这通常是一件好事,直到你运行基准测试。在那里,你想要计算,但你不需要结果。本质上,你观察基准测试所占用的“资源”,但没有简单的方法可以与编译器争论这一点。

比如下面的测试用例,该方法中只涉及到变量x和变量y的计算,但是该方法没有返回值,也就是说,该方法是不需要执行的,具体代码如下:

int x, y;

@Benchmark
public void test_dead() {
  int r = x + y;
}

编译后的汇编指令如下:

  1.72%  ↗  ...370: movzbl 0x94(%r9),%r10d  ; load $isDone
  2.06%  │  ...378: mov    0x348(%r15),%r11 ; safepoint poll, part 1
 27.91%  │  ...37f: add    $0x1,%rbp        ; ops++;
 28.56%  │  ...383: test   %eax,(%r11)      ; safepoint poll, part 2
 33.43%  │  ...386: test   %r10d,%r10d      ; are we done? spin back if not.
         ╰  ...389: je     ...370

从上述的结果来看,只有基准测试相关的汇编代码,并没有x+y相关的汇编指令。

纯 Java 黑洞

从始至今,JMH 就通过接受基准测试的结果来提供避免消除不会运行的代码代码的方法。在底层,这是通过将结果输入到Blackhole中来实现的,在某些情况下也可以直接使用。

简而言之,Blackhole 必须对传入的参数实现一个副作用:假装它被使用。Blackhole实现说明描述了 Blackhole 实现者在尝试与编译器合作时必须处理的问题。高效实现它是近 JVM 工程的一项很好的实践方向。

无论如何,所有这些混乱对于 JMH 用户来说都是隐藏的,因此他们可以这样做:

int x, y;

@Benchmark
public int test_return() {
  return x + y;
}

但是,如果你查看生成的代码,你会看到计算和Blackhole代码都在那里,具体的汇编代码如下:

main loop:
  2.09% ↗ ...e32: mov    0x40(%rsp),%r10   ; load $this
  7.46% │ ...e37: mov    0x10(%r10),%edx   ; load $this.x
  0.64% │ ...e3b: add    0xc(%r10),%edx    ; add $this.y
  2.11% │ ...e3f: mov    0x38(%rsp),%rsi   ; call Blackhole.consume
  1.74% │ ...e44: data16 xchg %ax,%ax
  6.52% │ ...e47: callq  ...a80
 18.37% │ ...e4c: mov    (%rsp),%r10
  1.50% │ ...e50: movzbl 0x94(%r10),%r11d  ; load $isDone
  2.85% │ ...e58: mov    0x348(%r15),%r10  ; safepoint poll, part 1
  6.74% │ ...e5f: add    $0x1,%rbp         ; ops++
  0.62% │ ...e63: test   %eax,(%r10)       ; safepoint poll, part 2
  0.66% │ ...e66: test   %r11d,%r11d       ; are we done? spin back if not.
        ╰ ...e69: je     ...e32
Blackhole.consume:
  2.34%   ...040: mov    %eax,-0x14000(%rsp) ; too
  9.14%   ...047: push   %rbp                ; lazy
  0.64%   ...048: sub    $0x20,%rsp          ; to
  3.38%   ...04c: mov    %edx,%r11d          ; cross-reference
  6.66%   ...04f: xor    0xb0(%rsi),%r11d    ; this
  0.68%   ...056: mov    %edx,%r8d           ; with
  1.76%   ...059: xor    0xb8(%rsi),%r8d     ; the
  1.62%   ...060: cmp    %r8d,%r11d          ; actual
        ╭ ...063: je     ...078              ; Blackhole
  7.22% │ ...065: add    $0x20,%rsp          ; code
  0.35% │ ...069: pop    %rbp
  2.01% │ ...06a: cmp    0x340(%r15),%rsp
        │ ...071: ja     ...094
  8.53% │ ...077: retq
        ↘ ...078: mov    %rsi,%rbp

毫不奇怪, Blackhole成本在如此小的基准测试中占据主导地位。使用-prof perfnorm ,我们可以看到它有多糟糕:

Benchmark                            Mode  Cnt   Score    Error  Units
XplusY.test_return                   avgt   25   3.288 ±  0.032  ns/op
XplusY.test_return:L1-dcache-loads   avgt    5  13.092 ±  0.487   #/op
XplusY.test_return:L1-dcache-stores  avgt    5   3.031 ±  0.076   #/op
XplusY.test_return:branches          avgt    5   5.031 ±  0.089   #/op
XplusY.test_return:cycles            avgt    5   8.781 ±  0.351   #/op
XplusY.test_return:instructions      avgt    5  27.162 ±  0.489   #/op

也就是说,我们的“有效负载”只有 2 条指令,但整个基准测试还需要另外 25 条指令!是的,现代 CPU 可以在大约 9 个周期内执行这一大堆指令,但这仍然太过繁重。更糟糕的是,调用代码和相关的堆栈管理引入了存储。

基准测试本身大约需要 3.2 ns/op,这为我们能够可靠测量的效果设置了下限。

编译器黑洞

我们可以要求编译器提供更直接的配合,使用编译器黑洞。这些是在 OpenJDK 17 中通过JDK-8259316实现的,并计划将其反向移植到 11u。编译器黑洞指示编译器在优化阶段携带所有参数,然后在发出生成的代码时最终丢弃它们。然后,只要硬件本身不给我们带来意外,我们就应该没问题。

它们应该对 JMH 用户透明地工作,但由于整个过程尚处于实验阶段,因此目前 JMH 用户需要使用-Djmh.blackhole.mode=COMPILER选择加入编译器黑洞,然后检查生成的代码是否正确。 事实上,在我们的基准测试中使用编译器黑洞,我们可以看到计算仍然存在,并且不再有Blackhole调用!

  8.95% ↗ ...c00: mov    0x10(%r11),%r10d  ; load $this.x
  0.36% │ ...c04: add    0xc(%r11),%r10d   ; add $this.y
        │                                  ; (AND COMPILER BLACKHOLE IT)
  0.94% │ ...c08: movzbl 0x94(%r14),%r8d   ; load $isDone
 26.76% │ ...c10: mov    0x348(%r15),%r10  ; safepoint poll, part 1
  8.42% │ ...c17: add    $0x1,%rbp         ; ops++
  0.43% │ ...c1b: test   %eax,(%r10)       ; safepoint poll, part 2
 46.96% │ ...c1e: test   %r8d,%r8d         ; are we done? spin back if not.
  0.02% ╰ ...c21: je     ...c00

除了在扩展的反汇编注释中,你甚至无法在任何地方看到黑洞代码,但它的效果就在那里:计算被保留了下来。 -prof perfnorm所展现的效果会更加明显:

Benchmark                            Mode  Cnt   Score    Error  Units
XplusY.test_return                   avgt   25   0.963 ±  0.042  ns/op
XplusY.test_return:L1-dcache-loads   avgt    5   5.029 ±  0.170   #/op
XplusY.test_return:L1-dcache-stores  avgt    5   0.001 ±  0.002   #/op
XplusY.test_return:branches          avgt    5   1.006 ±  0.019   #/op
XplusY.test_return:cycles            avgt    5   2.569 ±  0.108   #/op
XplusY.test_return:instructions      avgt    5   8.043 ±  0.182   #/op

不再有存储,只有 6 条附加指令用于承载基础设施。整个基准测试能够在不到 3 个周期和不到 1 纳秒的时间内完成,这涉及 5 次 L1 访问,其中 3 次是基础设施访问。

这也使得显式使用Blackhole更加方便,例如在执行循环时:

@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 CPI_Floor {

    @Param({"1000000"})
    private int count;

    @Benchmark
    public void test(Blackhole bh) {
        for (int c = 0; c < count; c += 10000) {
            for (int k = 0; k < 10000; k++) {
                int v = k + k + k;
                bh.consume(v);
            }
        }
    }
}

在 TR 3970X 上,这达到了 CPI 下限或 ~0.16 clks/insn 或 IPC 上限 ~6 insn/clk!事实上,看起来整个“k”内循环恰好在一个周期内执行!

Benchmark                    (count)  Mode        Score   Error         Units
CPI_Floor.test               1000000  avgt   273422.337 ± 12722.427     ns/op
CPI_Floor.test:CPI           1000000  avgt        0.169             clks/insn
CPI_Floor.test:IPC           1000000  avgt        5.907             insns/clk
CPI_Floor.test:branches      1000000  avgt  1003135.103                  #/op
CPI_Floor.test:cycles        1000000  avgt  1022821.963                  #/op
CPI_Floor.test:instructions  1000000  avgt  6042142.469                  #/op

总结

编译器黑洞非常适合低级性能调查。尝试使用它们,检查它们是否按您的要求运行,展示成功和失败案例,并希望所有这些最终都会在 JMH 中出现新的默认黑洞模式。


http://www.kler.cn/a/600107.html

相关文章:

  • 数学建模:MATLAB卷积神经网络
  • Langchain 自定义工具和内置工具
  • FRP结合Nginx实现HTTPS服务穿透
  • LVGL移植详细教程(基于STM32F407+rt-thread+FSMC接口屏+V9版本)
  • java 设置操作系统编码、jvm平台编码和日志文件编码都为UTF-8的操作方式
  • 现代化前端异常治理与容灾体系深度解析
  • 本周安全速报(2025.3.18~3.24)
  • VSCODE上ckg_server_linux进程占用CPU过多
  • C++红黑树的深入解析:从理论到实践
  • Mysql--日志(错误日志、二进制日志、查询日志、慢查询日志)
  • Wireshark网络抓包分析使用详解
  • SAP SD学习笔记34 - 预詑品(寄售物料)之 预詑品返品(KR),预詑品引取(KA)
  • 青少年编程与数学 02-011 MySQL数据库应用 16课题、安全机制
  • js 中 如何获取数组的交集【面试题】
  • 如何为AI开发选择合适的服务器?
  • 《HarmonyOS Next群头像拼接与组件截图技术解析》
  • 第六届IEEE人工智能、网络与信息技术国际学术会议(AINIT 2025)
  • “我是GM”之NAS搭建Luanti游戏服务器,开启沙盒游戏新体验
  • WPS中把多张PPT打印到一张A4纸上
  • Jenkins 共享库(Shared Libraries)使用说明文档