Unl*_*kus 4 assembly gcc sse x86-64 micro-optimization
考虑以下代码:
double x(double a,double b) {
return a*(float)b;
}
Run Code Online (Sandbox Code Playgroud)
它做了一个转换形式double
,float
然后再double
乘以。
当我gcc 9.1
用-O3
on编译它时,x86/64
我得到:
x(double, double):
movapd xmm2, xmm0
pxor xmm0, xmm0
cvtsd2ss xmm1, xmm1
cvtss2sd xmm0, xmm1
mulsd xmm0, xmm2
ret
Run Code Online (Sandbox Code Playgroud)
使用clang
和旧版本的gcc
我得到这个:
x(double, double):
cvtsd2ss xmm1, xmm1
cvtss2sd xmm1, xmm1
mulsd xmm0, xmm1
ret
Run Code Online (Sandbox Code Playgroud)
在这里我不抄xmm0
成xmm2
,这似乎不需要我。
随着gcc 9.1
和-Os
我得到:
x(double, double):
movapd xmm2, xmm0
cvtsd2ss xmm1, xmm1
cvtss2sd xmm0, xmm1
mulsd xmm0, xmm2
ret
Run Code Online (Sandbox Code Playgroud)
所以它只是删除设置xmm0
为零的指令而不是moveapd
.
我相信所有三个版本都是正确的,那么该gcc 9.1 -O3
版本会带来性能优势吗?如果是,为什么?请问pxor xmm0, xmm0
指令有什么好处?
该问题类似于优化 C 代码中的汇编代码冗余,但我认为不同,因为旧版本gcc
不会生成不必要的副本。
这是 GCC 遗漏的优化;不幸的是,当GCC的寄存器分配器在调用约定强加的硬寄存器约束下做得很差时,这对于小函数中的 GCC 来说并不罕见;显然 GCC 在较大函数的部分之间通常不会像这样愚蠢。
所述pxor
-zeroing有打破的(假)输出相关cvtss2sd
,它存在由于英特尔的短视设计用于单一来源标量指令离开未修饰的目标向量的上部。他们从 PIII 的 SSE1 开始,在那里它提供了短期收益,因为 PIII 将 XMM regs 处理为两个 64 位一半,所以只写一半让指令像sqrtss
单 uop。
但不幸的是,即使对于 SSE2(奔腾 4 的新功能),他们也保留了这种模式。后来拒绝使用 AVX 版本的 SSE 指令修复它。因此,编译器不得不在通过错误依赖创建长循环依赖链的风险或使用像素归零的风险之间做出选择。GCC 总是保守地使用 pxor at -O3
,在省略它-Os
。(像 2-source 操作mulsd
已经依赖于目标作为输入,所以这是不必要的)。
在这种情况下,由于其寄存器分配选择pxor
不当,省略-zeroing 将意味着在准备好之前无法开始转换(float)b
回。因此,如果关键路径已准备好(提前准备好),则省略它会使Skylake 上的 ->result延迟增加5 个周期(仅在准备好后才能运行2-uop ,因为输出必须合并到寄存器中)原来是这样的。)否则只是必须等待,所有涉及的事情都提前完成。double
a
a
b
a
cvtss2sd
a
a
mulsd
a
b
foo same,same
是另一种解决输出依赖性的方法;这就是 clang 正在做的事情。(以及 GCC 试图为 做什么popcnt
,它出乎意料地在 Sandybridge-family 上有一个在架构上不需要的,与这些愚蠢的 SSE 不同。)
顺便说一句,AVX 3 操作数指令有时确实提供了一种解决错误依赖项的方法,使用“冷”寄存器或异或零寄存器作为要合并到的寄存器。包括标量 int->FP,虽然 clang 有时只是使用movd
加压缩转换。
相关:为什么添加 xorps 指令使这个函数使用 cvtsi2ss 并添加 ~5x 快? (我应该把它链接起来,我忘了我最近已经在 Stack Overflow 上详细地写了这个。)
在movapd
和pxor
归零不用花费在现代CPU任何延迟,但没有什么是永远免费的。它们仍然需要前端 uop 和代码大小(L1i 缓存占用空间)。 movapd
后端具有零延迟,并且不需要执行单元,仅此而已 - x86 的 MOV 真的可以“免费”吗?为什么我完全不能重现这个?
归档时间: |
|
查看次数: |
93 次 |
最近记录: |