为什么添加 xorps 指令会使使用 cvtsi2ss 和 addss 的函数快 ~5 倍?
Why does adding an xorps instruction make this function using cvtsi2ss and addss ~5x faster?
我正在使用 Google Benchmark 优化一个函数,运行 导致我的代码在某些情况下意外变慢。我开始试验它,查看编译后的程序集,并最终想出了一个展示问题的最小测试用例。这是我提出的显示这种减速的程序集:
.text
test:
#xorps %xmm0, %xmm0
cvtsi2ss %edi, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
retq
.global test
此函数遵循 GCC/Clang 函数声明的 x86-64 调用约定 extern "C" float test(int);
请注意注释掉的 xorps
指令。取消注释该指令会显着提高函数的性能。使用我的机器和 i7-8700K 对其进行测试,Google 基准显示函数 没有 xorps
指令需要 8.54ns(CPU),而函数 with xorps
指令需要 1.48ns。我已经在具有各种 OS、处理器、处理器代数和不同处理器制造商(Intel 和 AMD)的多台计算机上对此进行了测试,它们都表现出相似的性能差异。重复 addss
指令会使减速更加明显(在一定程度上),并且使用此处的其他指令(例如 mulss
)甚至混合指令仍然会发生这种减速,只要它们都依赖于%xmm0
中的值以某种方式。值得指出的是,只有调用 xorps
each 函数调用才能提高性能。使用循环(如 Google Benchmark 所做的那样)和循环外的 xorps
调用对性能进行采样仍然显示较慢的性能。
因为这是 专门添加 指令提高性能的情况,这似乎是由 CPU 中非常低级的东西引起的。由于它出现在各种各样的 CPU 中,所以这似乎一定是故意的。但是,我找不到任何文档来解释为什么会发生这种情况。有人对这里发生的事情有解释吗?这个问题似乎取决于复杂的因素,因为我在原始代码中看到的减速只发生在特定的优化级别(-O2,有时是 -O1,但不是 -Os),没有内联,并且使用特定编译器(Clang,但不是 GCC)。
cvtsi2ss %edi, %xmm0
将浮点数合并到 XMM0 的低元素中,因此它对旧值具有错误的依赖性。(重复调用同一函数,创建一个长循环携带的依赖链。)
xor-zeroing 破坏了 dep 链,允许无序执行发挥它的魔力。所以你的瓶颈是 addss
吞吐量(0.5 个周期)而不是延迟(4 个周期)。
您的 CPU 是 Skylake 衍生产品,所以这些是数字;较早的英特尔使用专用的 FP-add 执行单元而不是 FMA 单元上的 运行 具有 3 个周期延迟和 1 个周期吞吐量。 https://agner.org/optimize/。可能函数 call/ret 开销阻止您看到流水线 FMA 单元中 8 个飞行中 addss
微指令的延迟 * 带宽乘积的完整 8 倍预期加速;如果您从单个函数内的循环中删除 xorps
dep-breaking,您应该会得到加速。
GCC 往往非常 "careful" 关于错误的依赖关系 ,花费额外的指令(前端带宽)来打破它们以防万一。在前端瓶颈的代码中(或者总代码大小/uop 缓存占用空间是一个因素)如果寄存器实际上及时准备好,这会降低性能。
Clang/LLVM 对此鲁莽而漫不经心,通常不会费心去避免对未写入当前函数的寄存器的错误依赖。 (即假设/假装寄存器在函数入口处是 "cold")。正如您在评论中所展示的那样,当在一个函数内循环时,clang 确实避免了通过异或归零创建循环携带的 dep 链,而不是通过对同一函数的多次调用。
Clang 甚至在某些情况下无缘无故地使用 8 位 GP 整数部分寄存器,与 32 位寄存器相比,这不会节省任何代码大小或指令。通常它可能没问题,但是如果调用者(或同级函数调用)仍然有缓存未命中加载到该 reg 时,则存在耦合到长 dep 链或创建循环携带的依赖链的风险例如,调用。
参见 for more about how OoO exec can overlap short to medium length independent dep chains. Also related: 是关于展开具有多个累加器的点积以隐藏 FMA 延迟。
https://www.uops.info/html-instr/CVTSI2SS_XMM_R32.html 具有此指令在各种 uarche 中的性能详细信息。
如果你可以使用 AVX,你可以避免这种情况,vcvtsi2ss %edi, %xmm7, %xmm0
(其中 xmm7 是你最近没有写过的任何寄存器,或者它在导致 EDI 当前值的 dep 链)。
正如我在
中提到的
This ISA design wart is thanks to Intel optimizing for the short term with SSE1 on Pentium III. P3 handled 128-bit registers internally as two 64-bit halves. Leaving the upper half unmodified let scalar instructions decode to a single uop. (But that still gives PIII sqrtss
a false dependency). AVX finally lets us avoid this with vsqrtsd %src,%src, %dst
at least for register sources if not memory, and similarly vcvtsi2sd %eax, %cold_reg, %dst
for the similarly near-sightedly designed scalar int->fp conversion instructions.
(GCC missed-optimization reports: 80586, 89071, 80571.)
如果 cvtsi2ss
/sd
将寄存器的高位元素归零,我们就不会遇到这个愚蠢的问题/不需要散布异或归零指令;感谢英特尔。 (另一种策略是使用 SSE2 movd %eax, %xmm0
,其中 做 零扩展,然后打包 int->fp 转换,对整个 128 位向量进行操作。这可以收支平衡float,其中 int->fp 标量转换为 2 微指令,向量策略为 1+1。但不是 double,其中 int->fp 打包转换花费 shuffle + FP 微指令。)
这正是 AMD64 通过对 32 位整数寄存器的写入隐式零扩展到完整的 64 位寄存器而不是保持不变(也称为合并)来避免的问题。 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?(写入 8 位和 16 位寄存器 do 导致对 AMD CPUs 和自 Haswell 以来的 Intel 的错误依赖。
我正在使用 Google Benchmark 优化一个函数,运行 导致我的代码在某些情况下意外变慢。我开始试验它,查看编译后的程序集,并最终想出了一个展示问题的最小测试用例。这是我提出的显示这种减速的程序集:
.text
test:
#xorps %xmm0, %xmm0
cvtsi2ss %edi, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
addss %xmm0, %xmm0
retq
.global test
此函数遵循 GCC/Clang 函数声明的 x86-64 调用约定 extern "C" float test(int);
请注意注释掉的 xorps
指令。取消注释该指令会显着提高函数的性能。使用我的机器和 i7-8700K 对其进行测试,Google 基准显示函数 没有 xorps
指令需要 8.54ns(CPU),而函数 with xorps
指令需要 1.48ns。我已经在具有各种 OS、处理器、处理器代数和不同处理器制造商(Intel 和 AMD)的多台计算机上对此进行了测试,它们都表现出相似的性能差异。重复 addss
指令会使减速更加明显(在一定程度上),并且使用此处的其他指令(例如 mulss
)甚至混合指令仍然会发生这种减速,只要它们都依赖于%xmm0
中的值以某种方式。值得指出的是,只有调用 xorps
each 函数调用才能提高性能。使用循环(如 Google Benchmark 所做的那样)和循环外的 xorps
调用对性能进行采样仍然显示较慢的性能。
因为这是 专门添加 指令提高性能的情况,这似乎是由 CPU 中非常低级的东西引起的。由于它出现在各种各样的 CPU 中,所以这似乎一定是故意的。但是,我找不到任何文档来解释为什么会发生这种情况。有人对这里发生的事情有解释吗?这个问题似乎取决于复杂的因素,因为我在原始代码中看到的减速只发生在特定的优化级别(-O2,有时是 -O1,但不是 -Os),没有内联,并且使用特定编译器(Clang,但不是 GCC)。
cvtsi2ss %edi, %xmm0
将浮点数合并到 XMM0 的低元素中,因此它对旧值具有错误的依赖性。(重复调用同一函数,创建一个长循环携带的依赖链。)
xor-zeroing 破坏了 dep 链,允许无序执行发挥它的魔力。所以你的瓶颈是 addss
吞吐量(0.5 个周期)而不是延迟(4 个周期)。
您的 CPU 是 Skylake 衍生产品,所以这些是数字;较早的英特尔使用专用的 FP-add 执行单元而不是 FMA 单元上的 运行 具有 3 个周期延迟和 1 个周期吞吐量。 https://agner.org/optimize/。可能函数 call/ret 开销阻止您看到流水线 FMA 单元中 8 个飞行中 addss
微指令的延迟 * 带宽乘积的完整 8 倍预期加速;如果您从单个函数内的循环中删除 xorps
dep-breaking,您应该会得到加速。
GCC 往往非常 "careful" 关于错误的依赖关系 ,花费额外的指令(前端带宽)来打破它们以防万一。在前端瓶颈的代码中(或者总代码大小/uop 缓存占用空间是一个因素)如果寄存器实际上及时准备好,这会降低性能。
Clang/LLVM 对此鲁莽而漫不经心,通常不会费心去避免对未写入当前函数的寄存器的错误依赖。 (即假设/假装寄存器在函数入口处是 "cold")。正如您在评论中所展示的那样,当在一个函数内循环时,clang 确实避免了通过异或归零创建循环携带的 dep 链,而不是通过对同一函数的多次调用。
Clang 甚至在某些情况下无缘无故地使用 8 位 GP 整数部分寄存器,与 32 位寄存器相比,这不会节省任何代码大小或指令。通常它可能没问题,但是如果调用者(或同级函数调用)仍然有缓存未命中加载到该 reg 时,则存在耦合到长 dep 链或创建循环携带的依赖链的风险例如,调用。
参见
https://www.uops.info/html-instr/CVTSI2SS_XMM_R32.html 具有此指令在各种 uarche 中的性能详细信息。
如果你可以使用 AVX,你可以避免这种情况,vcvtsi2ss %edi, %xmm7, %xmm0
(其中 xmm7 是你最近没有写过的任何寄存器,或者它在导致 EDI 当前值的 dep 链)。
正如我在
This ISA design wart is thanks to Intel optimizing for the short term with SSE1 on Pentium III. P3 handled 128-bit registers internally as two 64-bit halves. Leaving the upper half unmodified let scalar instructions decode to a single uop. (But that still gives PIII
sqrtss
a false dependency). AVX finally lets us avoid this withvsqrtsd %src,%src, %dst
at least for register sources if not memory, and similarlyvcvtsi2sd %eax, %cold_reg, %dst
for the similarly near-sightedly designed scalar int->fp conversion instructions.
(GCC missed-optimization reports:80586,89071, 80571.)
如果 cvtsi2ss
/sd
将寄存器的高位元素归零,我们就不会遇到这个愚蠢的问题/不需要散布异或归零指令;感谢英特尔。 (另一种策略是使用 SSE2 movd %eax, %xmm0
,其中 做 零扩展,然后打包 int->fp 转换,对整个 128 位向量进行操作。这可以收支平衡float,其中 int->fp 标量转换为 2 微指令,向量策略为 1+1。但不是 double,其中 int->fp 打包转换花费 shuffle + FP 微指令。)
这正是 AMD64 通过对 32 位整数寄存器的写入隐式零扩展到完整的 64 位寄存器而不是保持不变(也称为合并)来避免的问题。 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?(写入 8 位和 16 位寄存器 do 导致对 AMD CPUs 和自 Haswell 以来的 Intel 的错误依赖。