0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

为什么在JVM x86平台生成的机器代码中会看到XMM寄存器?

冬至子 来源:ImportNew 作者:ImportNew 2023-06-15 15:22 次阅读

2. 问题

代码中没有浮点或矢量操作,为什么在 JVM x86 平台生成的机器代码中会看到 XMM 寄存器

3. 理论

FPU 和矢量单元在现代 CPU 中无处不在。通常,它们会为 FPU 特定操作提供了备用寄存器。例如,英特尔 x86_64 平台的 SSE 和 AVX 扩展包含了一组丰富的 XMM、YMM 和 ZMM 寄存器供指令操作。

虽然非矢量指令集与矢量、非矢量寄存器通常不会正交,比如不能在 x86_64 上对 XMM 寄存器执行通用 IMUL,但是这些寄存器仍然提供了一种存储选项。即使不用于矢量计算,也可以在这些寄存器中存储数据。

(1) 最极端的情况是把矢量寄存器当缓冲用。

寄存器分配器的任务是在一个特定的编译单元(比如方法)中获取程序需要的所有操作数,并为它们分配寄存器——映射到机器实际寄存器。真实程序中,需要的操作数大于机器中可用的寄存器数目。这时寄存器分配器必须把一些操作数放到寄存器以外的某个地方(比如堆栈),也就是说会发生操作数溢出。

x86_64 上有16个通用寄存器(并非每个寄存器都可用)。目前,大多数机器还有16个 AVX 寄存器。发生溢出时,可以不存储到堆栈而存储到 XMM 寄存器中吗?答案是可以。这么做会带来什么好处?

4. 实验

看看下面这个简单的 JMH 基准测试,用一种非常特殊的方式构建基准(简单起见,这里假设 Java 具备有预处理能力):

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 ORDERED
    public void ordered() {
#else
    public void unordered() {
#endif
        int 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 ORDERED
        dg = vsg; // 给 optimizer 制造点麻烦
#else
        dg = sg;  // 只做常规存储
#endif
        d00 = 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  Units
FPUSpills.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对 load-store,实际测试中大致有25对,但是这里没有25个通用寄存器!从 perfasm 结果中可以看到,优化器会把临近的 load-store 对合并,减小寄存器压力:

0.38%    0.28%  ↗  movzbl 0x94(%rcx),%r9d
                │  ...
0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; 读取字段 s00
0.04%    0.02%  │  mov    %r10d,0x70(%r8)    ; 存储字段 d00
                │  ...
                │  ... (transfer repeats for multiple vars) ...
                │  ...
                ╰  je     BACK

ordered 测试会给优化器制造一点混乱,在存储前全部加载。上面的结果也印证了这一点:先全部加载,再全部存储。加载全部完成时寄存器的压力最大,这时还没有开始存储。即便如此,从结果来看与 unordered 差异不大:

Benchmark                                  Mode  Cnt   Score    Error  Units
FPUSpills.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
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

这是因为已经设法把操作数溢出到 XMM 寄存器中,而不是在堆栈上存储:

3.08%    3.79%  ↗  vmovq  %xmm0,%r11
                │  ...
0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; 读取字段 s00
0.02%           │  vmovd  %r10d,%xmm4        ; < --- FPU 溢出
0.25%    0.20%  │  mov    0x10(%r11),%r10d   ; 读取字段 s01
0.02%           │  vmovd  %r10d,%xmm5        ; < --- FPU 溢出
                │  ...
                │  ... (读取更多字段和 XMM 溢出) ...
                │  ...
0.12%    0.02%  │  mov    0x60(%r10),%r13d   ; 读取字段 s21
                │  ...
                │  ... (读取到寄存器) ...
                │  ...
                │  ------- 读取完成, 开始写操作 ------
0.18%    0.16%  │  mov    %r13d,0xc4(%rdi)   ; 存储字段 d21
                │  ...
                │  ... (读寄存器并存储字段)
                │  ...
2.77%    3.10%  │  vmovd  %xmm5,%r11d        : < --- FPU 取消溢出
0.02%           │  mov    %r11d,0x78(%rdi)   ; 存储字段 d01
2.13%    2.34%  │  vmovd  %xmm4,%r11d        ; < --- FPU 取消溢出
0.02%           │  mov    %r11d,0x70(%rdi)   ; 存储字段 d00
                │  ...
                │  ... (取消溢出并存储字段)
                │  ...
                ╰  je     BACK

请注意:这里的确对某些操作数使用了通用寄存器(GPR),但是当所有寄存器被用完时会发生溢出。这里对时机的描述并不确切。看起来先发生了溢出,然后使用 GPR。然而这是一个假象,因为寄存器分配器是在全局进行分配。

(2) 一些寄存器分配器实际执行的是线性分配,提高了 regalloc 的速度与生成代码的效率。

XMM 溢出延迟似乎是最小的:尽管溢出需要更多指令,但它们的执行效率很高能够有效弥补流水线的缺陷。通过34条额外指令,大约17条溢出指令对,实际只要求4个额外周期。请注意,按照 4/34 = ~0.11 时钟/指令 计算 CPI 是不对的,计算结果会超出当前 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

上面的结果可以看到 load/store 计数增加,为什么?这些是堆栈溢出。虽然堆栈本身速度很快,但仍然在内存中运行,访问 L1 缓存中的堆栈空间。基本上大约需要额外17个存储对,但现在只需要约11个时钟周期。这里 L1 缓存的吞吐量是主要限制。

最后,可以观察 -XX:-UseFPUForSpilling 的 perfasm 输出:

2.45%    1.21%  ↗  mov    0x70(%rsp),%r11
                │  ...
0.50%    0.31%  │  mov    0xc(%r11),%r10d    ; 读取字段 s00
0.02%           │  mov    %r10d,0x10(%rsp)   ; < --- 堆栈溢出!
2.04%    1.29%  │  mov    0x10(%r11),%r10d   ; 读取字段 s01
                │  mov    %r10d,0x14(%rsp)   ; < --- 堆栈溢出!
                │  ...
                │  ... (读取其它字段和堆栈溢出) ...
                │  ...
0.12%    0.19%  │  mov    0x64(%r10),%ebp    ; 读取字段 s22
                │  ...
                │  ... (more reads into registers) ...
                │  ...
                │  ------- 读取完成, 开始写操作 ------
3.47%    4.45%  │  mov    %ebp,0xc8(%rdi)    ; 存储字段 d22
                │  ...
                │  ... (读取更多寄存器和存储字段)
                │  ...
1.81%    2.68%  │  mov    0x14(%rsp),%r10d   ; < --- 取消堆栈溢出
0.29%    0.13%  │  mov    %r10d,0x78(%rdi)   ; 存储字段 d01
2.10%    2.12%  │  mov    0x10(%rsp),%r10d   ; < --- 取消堆栈溢出
                │  mov    %r10d,0x70(%rdi)   ; 存储字段 d00
                │  ...
                │  ... (取消其它溢出和存储字段)
                │  ...
                ╰  je     BACK

的确,在堆栈溢出发生的地方也可以看到 XMM 溢出。

5. 观察

FPU 溢出是缓解寄存器压力的一种好办法。虽然不增加通用寄存器寄存器数量,但确实在溢出时提供了更快的临时存储。在仅需要几个额外的溢出存储时,可以避免转存到 L1 缓存支持的堆栈。

这为什么有时会出现奇怪的性能差异:如果在一些关键路径上没有用到 FPU 溢出,很可能会看到性能下降。例如,引入一个 slow-path GC 屏障,假定会清除 FPU 寄存器,可能会让编译器回退到堆栈溢出,并不去尝试其它优化。

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

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • ARM
    ARM
    +关注

    关注

    134

    文章

    8653

    浏览量

    361829
  • 寄存器
    +关注

    关注

    30

    文章

    5032

    浏览量

    117745
  • 存储器
    +关注

    关注

    38

    文章

    7151

    浏览量

    162001
  • JVM
    JVM
    +关注

    关注

    0

    文章

    152

    浏览量

    12129
  • FPU
    FPU
    +关注

    关注

    0

    文章

    38

    浏览量

    20997
收藏 人收藏

    评论

    相关推荐

    #硬声创作季 #硬件 微机原理与接口技术-02.04.01 80x86基本寄存器2

    元器件寄存器硬件X86X86架构
    水管工
    发布于 :2022年09月27日 00:59:11

    #硬声创作季 #硬件 微机原理与接口技术-02.05.01 80x86基本寄存器3

    元器件寄存器硬件X86X86架构
    水管工
    发布于 :2022年09月27日 01:00:13

    x86硬件平台的最佳伴侣

    的服务领域,这我们能够得到的性价比最高的计算机,在业界的霸主地位无可替代。但x86平台的一个小小的瑕疵给希望pc工作高可靠场合的用户带来了不少的麻烦,即绝大部分
    发表于 08-29 10:23

    x86硬件平台的最佳伴侣

    的服务领域,这我们能够得到的性价比最高的计算机,在业界的霸主地位无可替代。但x86平台的一个小小的瑕疵给希望pc工作高可靠场合的用户带来了不少的麻烦,即绝大部分
    发表于 08-29 10:26

    基于ARM嵌入式平台X86译码SOC架构设计

    向解码模块发送信号,通过设置Communicate模块中的寄存器控制指令译码的工作:设置X86指令的起始地址;设置X86指令的终止地址;设置ARM指令的初始存放地址;设置ARM指令复
    发表于 11-08 14:37

    X86硬件设计系列知识分享

    论坛有很多嵌入式的技术资料,却几乎没有X86硬件(PC,Server 等等)设计技术资料。本人从事X86硬件及系统设计多年,总结了系列X86平台设计知识,逐步分享出来,期望能够对从事相
    发表于 10-17 12:18

    基于英特尔X86平台主板硬件设计系列知识分享

    前面的贴子发步了X86系统架构演变第一篇。还有2篇,今天另发一新贴,一起分享出来,欢迎下载:。更多的X86设计文章也发布微信号:超硬工程师。欢迎关注并一起分享和讨论。
    发表于 10-25 22:56

    解读x86、ARM和MIPS三种主流芯片架构

    代码记忆体里执行,新设计的处理,只需增加较少的电晶体就可以执行同样的指令集,也可以很快地编写新的指令集程式;二是拥有庞大的指令集,x86拥有包括双运算元格式、寄存器
    发表于 05-25 16:09

    x86平台架构如何为用户带来丰富的交互式驾驶体验?

    本文主要讲述了x86平台架构如何为用户带来丰富的交互式驾驶体验,而这是非PC兼容型平台难以实现的。
    发表于 05-14 06:45

    嵌入式X86和ARM各自都有哪些优缺点呢

    代码记忆体里执行;二是拥有庞大的指令集,拥有包括双运算元格式、寄存器寄存器寄存器到记忆体以及记忆体到寄存器的多种指令类型。
    发表于 12-14 09:21

    如何解决嵌入式linux中声明使用arm_gcc编译出来的却是x86平台程序的问题?

    Makefile中明明已经定义变量CC?=arm-linux-gnueabihf-gcc但编译出来的却是x86平台的程序原来问题出在了?=这个赋值符号上,该符号如果变量没有被赋值,那么使用等号后面的值,如果已经赋值过了,那么使
    发表于 12-27 07:27

    TarsARM平台上的移植是如何去实现的

    计时来实现,具体实现如下。原x86嵌汇编实现:支持ARM64平台后的实现:3 协程实现协程是一种用户态的轻量级线程,其调度完全由用户控制。因此,协程调度切换时需要用户自己将寄存器和栈
    发表于 03-30 11:30

    如果arm CHIP內建x86 decoder會能跑x86

    如果arm CHIP內建 x86 decoder 會能跑 x86?現一堆X86 cpu 有些都變 micro code ..用 risc 方式 那如果 ARM內建
    发表于 06-14 11:38

    arm64和x86服务上运行的耗时来发现Dockerarm64架构下的性能问题

    发现Dockerarm64架构下的性能问题。本文描述的性能测试是分别在一arm64和一x86服务上进行的,两个服务
    发表于 07-12 15:48

    微机原理笔记——x86寄存器

    8086 CPU中寄存器总共为14个,且均为16位。即 AX,BX,CX,DX,SP,BP,SI,DI,IP,FLAG,CS,DS,SS,ES 共 14 个。而这 14 个寄存器按照一定方式又分
    发表于 12-08 18:21 7次下载
    微机原理笔记——<b class='flag-5'>x86</b><b class='flag-5'>寄存器</b>