shipilev.net/jvm/anatomy-quarks/19-lock-elision/
代码中没有浮点或向量运算。为什么在JVM x86平台生成的机器码中会看到XMM寄存器?
FPU和矢量单元在现代CPU中无处不在。通常,它们为FPU的特定操作提供备用寄存器。例如,英特尔x86_64平台的SSE和AVX扩展包含一组丰富的XMM、YMM和ZMM寄存器,用于指令操作。
虽然非矢量指令集和矢量与非矢量寄存器通常不是正交的,例如通用IMUL不能在x86_64上的XMM寄存器上执行,但这些寄存器仍然提供了存储选项。即使不用于矢量计算,数据也可以存储在这些寄存器中。
最极端的情况是使用向量寄存器作为缓冲区。
寄存器分配器的任务是在一个特定的编译单元中获取程序所需的所有操作数,并为它们分配寄存器——映射到机器的实际寄存器。在实际程序中,所需的操作数大于机器中可用的寄存器数量。此时,寄存器分配器必须将一些操作数放在寄存器之外的某个地方,这意味着会发生操作数溢出。
x86_64上有16个通用寄存器。目前,大多数机器也有16个AVX寄存器。发生溢出时,是否可以存储在XMM寄存器而不是堆栈中?答案是肯定的。这会带来什么好处?
看看下面这个简单的JMH基准测试,并以一种非常特殊的方式构建基准:
import org . open JDK . jmh . annotations . *;
import Java . util . concurrent . TiME UNit;
@预热
@测量
@Fork
@基准模式
@OutputTimeUnit
@State
publicclassFPUSpills {
国际s00,s01,s02,s03,s04,s05,s06,s07,s08,s09
ints10、s11、s12、s13、s14、s15、s16、s17、s18、s19
ints20,s21,s22,s23,s24
intd00,d01,d02,d03,d04,d05,d06,d07,d08,d09
国际号码d10、d11、d12、d13、d14、d15、d16、d17、d18、d19
intd20,d21,d22,d23,d24
intsg
volatileintvsg
intdg
@基准
# if deformordered
publicvoidordered{
#否则
publicvoidunordered{
#endif
intv00 = s00intv01 = s01intv02 = s02intv03 = s03intv04 = s04
intv05 = s05intv06 = s06intv07 = s07intv08 = s08intv09 = s09
intv10 = s10intv11 = s11intv12 = s12intv13 = s13intv14 = s14
intv15 = s15intv16 = s16intv17 = s17intv18 = s18intv19 = s19
intv20 = s20intv21 = s21intv22 = s22intv23 = s23intv24 = s24
# if deformordered
dg = vsg//给优化器添麻烦
#否则
dg = sg//只做常规存储
#endif
d00 = v00d01 = v01d02 = v02d03 = v03d04 = v04
d05 = v05d06 = v06d07 = v07d08 = v08d09 = v09
D10 = v10;d11 = v11d12 = V12;d13 = v13d14 = v14
d15 = v15d16 = v16d17 = v17d18 = v18d19 = v19
d20 = v 20;d21 = v21d22 = v22d23 = v23d24 = v24
}
}
在上面的例子中,一次将读取和写入多对字段。事实上,优化器本身并不局限于特定的程序。事实上,这是在无序测试中观察到的结果:
基准模式分数误差单位
fpusfills . unordered avgt 156.961 0.002 ns/op
fpusfills . unordered:CPI avgt 30.458 0.024 #/op
fpusfills . unordered:L1-dcache-loads avgt 328.057 0.730 #/op
fpusfills . unordered:L1-dcache-stores avgt 326.082 1.235 #/op
fpusfills . unordered:cycles avgt 326.165 1.575 #/op
FPUSpills.unordered:指令avgt 357.099 0.971#/op
上面显示了26对加载存储,但实际测试中大约有25对,但这里没有25个通用寄存器!从perfasm结果可以看出,优化器将合并相邻的加载-存储对,以减少寄存器压力:
0.38% 0.28% ↗ movzbl 0x94,%r9d
│ ...
0.25% 0.20% │ mov 0xc,% r10d读取字段s00
0.04% 0.02% │ mov %r10d,0x 70;存储字段d00
│ ...
│ ......
│ ...
我回来了
有序测试会给优化器造成一点混乱,会在存储之前加载。以上结果也印证了这一点:先全部加载,再全部存储。加载完成时,寄存器压力最大,存储尚未开始。即便如此,结果与无序并没有太大区别:
基准模式分数误差单位
fpusfills . unordered avgt 156.961 0.002 ns/op
fpusfills . unordered:CPI avgt 30.458 0.024 #/op
fpusfills . unordered:L1-dcache-loads avgt 328.057 0.730 #/op
fpusfills . unordered:L1-dcache-stores avgt 326.082 1.235 #/op
fpusfills . unordered:cycles avgt 326.165 1.575 #/op
FPUSpills.unordered:指令avgt 357.099 0.971#/op
fpusfills . ordered avgt 157.961 0.008 ns/op
fpusfills . ordered:CPI avgt 30.329 0.026 #/op
订购:L1-dcache-loads avgt 329.070 1.361 #/op
订购:L1-dcache-stores avgt 326.131 2.243 #/op
fpusfills . ordered:cycles avgt 330.065 0.821 #/op
FPUSpills.ordered:说明avgt 391.449 4.839#/op
这是因为我们设法将操作数溢出到XMM寄存器中,而不是将它们存储在堆栈中:
3.08% 3.79% ↗ vmovq %xmm0,%r11
│ ...
0.25% 0.20% │ mov 0xc,% r10d读取字段s00
0.02%│vmovd % r10d %,xmm4& lt- FPU溢出
0.25% 0.20% │ mov 0x10,% r10d读取字段s01
0.02%│vmovd % r10d %,xmm5& lt- FPU溢出
│ ...
│ ......
│ ...
0.12% 0.02% │ mov 0x60,% r13d读取字段s21
│ ...
│ ......
│ ...
│ │ - .
0.18% 0.16% │ mov %r13d,0x C4;存储器字段d21
│ ...
│ ...
│ ...
2.77% 3.10% │ vmovd %xmm5,% r11d:& lt;- FPU取消溢出
0.02% │ mov %r11d,0x 78;存储字段d01
2.13% 2.34%│vmovd % XM 4,% r11d& lt- FPU取消溢出
0.02% │ mov %r11d,0x 70;存储字段d00
│ ...
│ ...
│ ...
我回来了
请注意,通用寄存器用于一些操作数,但当所有寄存器都用完时,就会发生溢出。这里对时机的描述并不确切。似乎是先发生溢出,再用GPR。但是,这是一种错觉,因为寄存器分配器是全局分配的。
一些寄存器分配器实际上执行线性分配,提高了regalloc的速度和代码生成的效率。
XMM溢出延迟似乎最小:虽然溢出需要更多的指令,但它们的执行效率非常高,可以有效弥补流水线的缺陷。有了34个额外指令和大约17个溢出指令对,实际上只需要4个额外周期。请注意,按照4/34 = ~0.11时钟/指令计算CPI是错误的,计算结果会超过当前CPU处理能力。然而,实际的改进是真实的,因为使用了以前没有使用过的执行块。
谈效率没有参考意义。这里用-XX:-UseFPUForSpilling让Hotspot禁用FPU溢出,这样就可以了解XMM溢出带来的好处:
基准模式分数误差单位
#默认
fpusfills . ordered avgt 157.961 0.008 ns/op
fpusfills . ordered:CPI avgt 30.329 0.026 #/op
订购:L1-dcache-loads avgt 329.070 1.361 #/op
订购:L1-dcache-stores avgt 326.131 2.243 #/op
fpusfills . ordered:cycles avgt 330.065 0.821 #/op
FPUSpills.ordered:说明avgt 391.449 4.839#/op
# -XX:-UseFPUForSpilling
fpusfills . ordered avgt 1510.976 0.003 ns/op
fpusfills . ordered:CPI avgt 30.455 0.053 #/op
订购:L1-dcache-loads avgt 347.327 5.113 #/op
订购:L1-dcache-stores avgt 341.078 1.887 #/op
fpusfills . ordered:cycles avgt 341.553 2.641 #/op
FPUSpills.ordered:说明avgt 391.264 7.312#/op
上述结果表明加载/存储数量增加。为什么?这些是堆栈溢出。尽管堆栈本身非常快,但它仍然在内存中运行,在L1缓存中的堆栈之间进行访问。基本上,大约需要17个额外的内存对,但现在只需要大约11个时钟周期。这里,L1缓存的吞吐量是主要限制。
最后,您可以观察到-XX的性能输出:-UseFPUForSpilling:
2.45% 1.21% ↗ mov 0x70,%r11
│ ...
0.50% 0.31% │ mov 0xc,% r10d读取字段s00
0.02% │ mov %r10d,0x 10;& lt-堆栈溢出!
2.04% 1.29% │ mov 0x10,% r10d读取字段s01
│ mov %r10d,0x 14;& lt-堆栈溢出!
│ ...
│ ......
│ ...
0.12% 0.19% │ mov 0x64,% ebp读取字段s22
│ ...
│ ......
│ ...
│ │ - .
3.47% 4.45% │ mov %ebp,0x c8;内存字段d22
│ ...
│ ...
│ ...
1.81% 2.68% │ mov 0x14,% r10d& lt-取消堆栈溢出
0.29% 0.13% │ mov %r10d,0x 78;存储字段d01
2.10% 2.12% │ mov 0x10,% r10d& lt-取消堆栈溢出
│ mov %r10d,0x 70;存储字段d00
│ ...
│ ...
│ ...
我回来了
事实上,在发生堆栈溢出的地方也可以看到XMM溢出。
FPU溢出是缓解寄存器压力的好方法。虽然通用寄存器的数量没有增加,但当发生溢出时,它确实提供了更快的临时存储。当只需要几个额外的溢出存储时,可以避免滚动到L1缓存支持的堆栈。
这就是为什么有时会出现奇怪的性能差异:如果一些关键路径上没有使用FPU溢出,很可能会出现性能下降。例如,引入慢路径GC屏障,假设FPU寄存器将被清除,可能会导致编译器退回堆栈溢出,而不尝试其他优化。
对于支持SSE、ARMv7和AArch64的x86平台,Hotspot默认启用-XX:+UseFPUForSpilling。所以不管你懂不懂这个技能,大部分程序都能从中受益。
1.《xmm 为什么 JVM x86 生成的机器代码有 XMM 寄存器?》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《xmm 为什么 JVM x86 生成的机器代码有 XMM 寄存器?》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/junshi/1686373.html