JVM常用概念之FPU溢出

devtools/2025/3/18 9:17:23/

问题

当自己的代码根本没有浮点或矢量运算,JVM在x86生成的机器代码为什么会用到XMM 寄存器?

基础知识

FPU 和矢量单元在现代 CPU 中随处可见,在许多情况下,它们为 FPU 特定的操作提供了一组备用寄存器。例如,Intel x86_64 中的 SSE 和 AVX 扩展具有一组额外的宽 XMM、YMM 和 ZMM 寄存器,可与更宽的指令结合使用。

虽然非矢量指令集通常与矢量和非矢量寄存器不正交(例如,我们不能在 x86_64 上将通用 IMUL 与 XMM 寄存器一起使用),但这些寄存器仍然提供了一个有趣的存储选项:我们可以将数据暂时存储在那里,即使该数据不用于矢量操作。

输入寄存器分配。寄存器分配器的职责是获取程序表示以及特定编译单元(例如方法)中程序所需的所有操作数,并将这些虚拟操作数映射到实际的机器寄存器 - 为它们分配寄存器。在许多实际程序中,给定程序位置的活动虚拟操作数的数量大于可用的机器寄存器数量。此时,寄存器分配器必须将一些操作数从寄存器中移到其他地方 - 例如堆栈上 - 即溢出操作数。

我们在 x86_64 上有 16 个通用寄存器(并非所有寄存器都可用),在大多数现代机器上还有 16 个 AVX 寄存器。我们可以溢出到 XMM 寄存器而不是堆栈吗?

实验

源码

import org.openjdk.jmh.annotations.*;import java.util.concurrent.TimeUnit;@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class FPUSpills {int s00, s01, s02, s03, s04, s05, s06, s07, s08, s09;int s10, s11, s12, s13, s14, s15, s16, s17, s18, s19;int s20, s21, s22, s23, s24;int d00, d01, d02, d03, d04, d05, d06, d07, d08, d09;int d10, d11, d12, d13, d14, d15, d16, d17, d18, d19;int d20, d21, d22, d23, d24;int sg;volatile int vsg;int dg;@Benchmark
#ifdef ORDEREDpublic void ordered() {
#elsepublic void unordered() {
#endifint v00 = s00; int v01 = s01; int v02 = s02; int v03 = s03; int v04 = s04;int v05 = s05; int v06 = s06; int v07 = s07; int v08 = s08; int v09 = s09;int v10 = s10; int v11 = s11; int v12 = s12; int v13 = s13; int v14 = s14;int v15 = s15; int v16 = s16; int v17 = s17; int v18 = s18; int v19 = s19;int v20 = s20; int v21 = s21; int v22 = s22; int v23 = s23; int v24 = s24;
#ifdef ORDEREDdg = vsg; // Confuse optimizer a little
#elsedg = sg;  // Just a plain store...
#endifd00 = v00; d01 = v01; d02 = v02; d03 = v03; d04 = v04;d05 = v05; d06 = v06; d07 = v07; d08 = v08; d09 = v09;d10 = v10; d11 = v11; d12 = v12; d13 = v13; d14 = v14;d15 = v15; d16 = v16; d17 = v17; d18 = v18; d19 = v19;d20 = v20; d21 = v21; d22 = v22; d23 = v23; d24 = v24;}
}

它一次读取和写入多对字段。优化器实际上并不依赖于特定的程序顺序。事实上,下述结果就是我们在unordered测试中观察到的情况:

Benchmark                                  Mode  Cnt   Score    Error  UnitsFPUSpills.unordered                        avgt   15   6.961 ±  0.002  ns/op
FPUSpills.unordered:CPI                    avgt    3   0.458 ±  0.024   #/op
FPUSpills.unordered:L1-dcache-loads        avgt    3  28.057 ±  0.730   #/op
FPUSpills.unordered:L1-dcache-stores       avgt    3  26.082 ±  1.235   #/op
FPUSpills.unordered:cycles                 avgt    3  26.165 ±  1.575   #/op
FPUSpills.unordered:instructions           avgt    3  57.099 ±  0.971   #/op

这给了我们大约 26 个加载-存储对,大致相当于我们在测试中的 25 对。但我们没有 25 个通用寄存器!Perfasm 表明优化器已将加载-存储对合并到彼此附近,因此寄存器压力要低得多:

0.38%    0.28%  ↗  movzbl 0x94(%rcx),%r9d│  ...0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; getfield s000.04%    0.02%  │  mov    %r10d,0x70(%r8)    ; putfield d00│  ...│  ... (transfer repeats for multiple vars) ...│  ...╰  je     BACK

我们想稍微误导优化器,并制造一点混乱,以便所有加载都在存储之前完成。这就是ordered测试所做的,在那里,我们可以看到加载和存储是批量发生的:首先是所有加载,然后是所有存储。在所有加载都已完成但尚未开始任何存储时,寄存器压力最高。即便如此,我们与unordered也没有显着差异:

Benchmark                                  Mode  Cnt   Score    Error  UnitsFPUSpills.unordered                        avgt   15   6.961 ±  0.002  ns/op
FPUSpills.unordered:CPI                    avgt    3   0.458 ±  0.024   #/op
FPUSpills.unordered:L1-dcache-loads        avgt    3  28.057 ±  0.730   #/op
FPUSpills.unordered:L1-dcache-stores       avgt    3  26.082 ±  1.235   #/op
FPUSpills.unordered:cycles                 avgt    3  26.165 ±  1.575   #/op
FPUSpills.unordered:instructions           avgt    3  57.099 ±  0.971   #/opFPUSpills.ordered                          avgt   15   7.961 ±  0.008  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.329 ±  0.026   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  29.070 ±  1.361   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  26.131 ±  2.243   #/op
FPUSpills.ordered:cycles                   avgt    3  30.065 ±  0.821   #/op
FPUSpills.ordered:instructions             avgt    3  91.449 ±  4.839   #/op

这是因为我们已成功将操作数溢出到XMM 寄存器中,而不是堆栈中,如下述执行结果所示:

3.08%    3.79%  ↗  vmovq  %xmm0,%r11│  ...0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; getfield s000.02%           │  vmovd  %r10d,%xmm4        ; <--- FPU SPILL0.25%    0.20%  │  mov    0x10(%r11),%r10d   ; getfield s010.02%           │  vmovd  %r10d,%xmm5        ; <--- FPU SPILL│  ...│  ... (more reads and spills to XMM registers) ...│  ...0.12%    0.02%  │  mov    0x60(%r10),%r13d   ; getfield s21│  ...│  ... (more reads into registers) ...│  ...│  ------- READS ARE FINISHED, WRITES START ------0.18%    0.16%  │  mov    %r13d,0xc4(%rdi)   ; putfield d21│  ...│  ... (more reads from registers and putfileds)...2.77%    3.10%  │  vmovd  %xmm5,%r11d        : <--- FPU UNSPILL0.02%           │  mov    %r11d,0x78(%rdi)   ; putfield d012.13%    2.34%  │  vmovd  %xmm4,%r11d        ; <--- FPU UNSPILL0.02%           │  mov    %r11d,0x70(%rdi)   ; putfield d00│  ...│  ... (more unspills and putfields)...╰  je     BACK

请注意,我们确实对某些操作数使用了通用寄存器 (GPR),但当它们耗尽时,我们就会溢出。“然后”在这里定义不明确,因为我们似乎先溢出,然后使用 GPR,但这是一种假象,因为寄存器分配器可能在完整图上运行。

XMM 溢出的延迟似乎很小:尽管我们确实声称溢出的指令更多,但它们的执行效率非常高,并填补了流水线空白:有了 34 条附加指令(即大约 17 对溢出),我们只声称增加了 4 个周期。请注意,将 CPI 计算为 4/34 = ~0.11 clk/insn 是不正确的,这将大于当前 CPU 的能力。但改进是真实的,因为我们使用了以前没有使用的执行块。

如果我们没有任何可比较的东西,那么效率的主张就毫无意义。但是在这里,我们有!我们可以使用-XX:-UseFPUForSpilling指示 Hotspot 避免使用 FPU 溢出,这让我们知道使用 XMM 溢出我们能获得多少好处:

Benchmark                                  Mode  Cnt   Score    Error  Units# Default
FPUSpills.ordered                          avgt   15   7.961 ±  0.008  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.329 ±  0.026   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  29.070 ±  1.361   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  26.131 ±  2.243   #/op
FPUSpills.ordered:cycles                   avgt    3  30.065 ±  0.821   #/op
FPUSpills.ordered:instructions             avgt    3  91.449 ±  4.839   #/op# -XX:-UseFPUForSpilling
FPUSpills.ordered                          avgt   15  10.976 ±  0.003  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.455 ±  0.053   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  47.327 ±  5.113   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  41.078 ±  1.887   #/op
FPUSpills.ordered:cycles                   avgt    3  41.553 ±  2.641   #/op
FPUSpills.ordered:instructions             avgt    3  91.264 ±  7.312   #/op

看到每个操作增加的加载/存储计数器了吗?这些是堆栈溢出:堆栈本身虽然很快,但仍驻留在内存中,因此访问 L1 缓存中的堆栈空间。它大致是相同的 17 个额外溢出对,但现在它们需要大约 11 个周期。L1 缓存的吞吐量是这里的限制因素。

最后,我们可以观察一下-XX:-UseFPUForSpilling的 perfasm 输出:

2.45%    1.21%  ↗  mov    0x70(%rsp),%r11│  ...0.50%    0.31%  │  mov    0xc(%r11),%r10d    ; getfield s000.02%           │  mov    %r10d,0x10(%rsp)   ; <--- stack spill!2.04%    1.29%  │  mov    0x10(%r11),%r10d   ; getfield s01│  mov    %r10d,0x14(%rsp)   ; <--- stack spill!...│  ... (more reads and spills to stack) ...│  ...0.12%    0.19%  │  mov    0x64(%r10),%ebp    ; getfield s22│  ...│  ... (more reads into registers) ...│  ...│  ------- READS ARE FINISHED, WRITES START ------3.47%    4.45%  │  mov    %ebp,0xc8(%rdi)    ; putfield d22│  ...│  ... (more reads from registers and putfields)...1.81%    2.68%  │  mov    0x14(%rsp),%r10d   ; <--- stack unspill0.29%    0.13%  │  mov    %r10d,0x78(%rdi)   ; putfield d012.10%    2.12%  │  mov    0x10(%rsp),%r10d   ; <--- stack unspill│  mov    %r10d,0x70(%rdi)   ; putfield d00│  ...│  ... (more unspills and putfields)...╰  je     BACK

从上述结果可以看出,堆栈溢出与 XMM 溢出的位置类似。

总结

FPU 溢出是缓解寄存器压力问题的好办法。虽然它不会增加可用于一般操作的寄存器数量,但它确实为溢出提供了更快的临时存储:因此,当我们只需要几个额外的溢出槽时,我们可以避免为此而跳转到 L1 缓存支持的堆栈。

这有时会导致有趣的性能偏差:如果某些关键路径上未使用 FPU 溢出,我们可能会看到性能下降。例如,引入慢路径 GC 屏障调用(假定会破坏 FPU 寄存器)可能会告诉编译器恢复到通常的基于堆栈的溢出,而无需尝试任何花哨的东西。

在 Hotspot 中,对于支持 SSE 的 x86 平台、ARMv7 和 AArch64, -XX:+UseFPUForSpilling默认启用。因此,无论您是否知道这个技巧,它都适用于大多数程序。


http://www.ppmy.cn/devtools/168030.html

相关文章

Android Room 框架测试模块源码深度剖析(五)

Android Room 框架测试模块源码深度剖析 一、引言 在 Android 开发中&#xff0c;数据持久化是一项重要的功能&#xff0c;而 Android Room 框架为开发者提供了一个强大且便捷的方式来实现本地数据库操作。然而&#xff0c;为了确保 Room 数据库操作的正确性和稳定性&#xf…

K8S学习之基础三十四:K8S之监控Prometheus部署pod版

使用 Kubernetes Pod 的方式部署 Prometheus 是一种常见的方法&#xff0c;尤其是在容器化和微服务架构中。以下是详细的步骤&#xff1a; 1. 创建命名空间&#xff08;可选&#xff09; 为了方便管理&#xff0c;可以为 Prometheus 创建一个单独的命名空间。 yaml 复制 a…

Spark 优化作业性能以及处理数据倾斜问题

1. 如何优化Spark作业的性能&#xff1f; 优化Spark作业性能可以从多个方面入手&#xff0c;以下是一些关键的优化策略&#xff1a; &#xff08;1&#xff09;资源调优 增加Executor数量&#xff1a;更多的Executor可以并行处理更多任务。 增加Executor内存&#xff1a;通过…

Zabbix安装(保姆级教程)

Zabbix 是一款开源的企业级监控解决方案,能够监控网络的多个参数以及服务器、虚拟机、应用程序、服务、数据库、网站和云的健康状况和完整性。它提供了灵活的通知机制,允许用户为几乎任何事件配置基于电子邮件的告警,从而能够快速响应服务器问题。Zabbix 基于存储的数据提供…

provide/inject源码实现

在 Vue 3 中&#xff0c;provide 和 inject 是通过 Vue 的响应式系统和组件实例机制实现的&#xff0c;底层是依赖 Vue 3 中的 Proxy 和 Reactive 来实现跨层级的数据传递和响应式绑定。以下是一个简化版的实现逻辑&#xff0c;帮助理解 Vue 3 中 provide 和 inject 是如何实现…

论文阅读:Deep Hybrid Camera Deblurring for Smartphone Cameras

今天介绍一篇 ACM SIGGRAPH 2024 的文章&#xff0c;关于手机影像中的去模糊的文章。 Deep Hybrid Camera Deblurring for Smartphone Cameras Abstract 手机摄像头尽管取得了显著的进步&#xff0c;但由于传感器和镜头较为紧凑&#xff0c;在低光环境下的成像仍存在困难&am…

【eNSP实战】基本ACL实现网络安全

拓扑图 要求&#xff1a; PC3不允许访问其他PC和Server1PC2允许访问Server1服务器&#xff0c;不允许其他PC访问各设备IP配置如图所示&#xff0c;这里不做展示 AR1接口vlan配置 vlan batch 10 20 30 # interface Vlanif10ip address 192.168.1.254 255.255.255.0 # inter…

C++ QT零基础教学(二)

一. 引子 在上一篇文章里面稍微讲解了一点C的基础&#xff0c;但是后面想了想要是还是继续单纯写C的内容的话要写到几百年年以后了&#xff0c;都不知道什么时候到QT了&#xff0c;所以这次就直接开始从QT开始描写了&#xff0c;当然肯定不会是很有难度&#xff0c;尽量还是会用…