我当时正忙着使用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
指令。取消注释该指令将极大地提高功能的性能。 Google基准测试使用我的机器在i7-8700K上对其进行测试,谷歌基准测试显示功能[[without xorps
指令耗时8.54ns(CPU),而功能with xorps
指令耗时1.48ns。我已经在具有不同操作系统,处理器,处理器世代和不同处理器制造商(英特尔和AMD)的多台计算机上进行了测试,它们都表现出相似的性能差异。重复执行addss
指令会使减慢更加明显(达到一定程度),并且只要它们全部取决于[mulss
)中的值,就仍然可以使用此处的其他指令(例如%xmm0
)或什至混合使用此减慢速度。 C0]。值得指出的是,仅调用xorps
each函数调用才能提高性能。在循环之外通过xorps
调用对循环进行性能采样(如Google Benchmark所做的那样)仍然显示性能较慢。
排他添加指令提高了性能,所以这似乎是由CPU中真正底层的原因引起的。由于它发生在各种各样的CPU上,因此这似乎是故意的。但是,我找不到任何解释这种情况发生的文档。有人对这里发生的事情有解释吗?这个问题似乎取决于复杂的因素,因为我在原始代码中看到的减速仅发生在特定的优化级别(-O2,有时是-O1,而不是-Os),没有内联并且使用特定的编译器(Clang) ,但不是GCC)。
[cvtsi2ss %edi, %xmm0
将float合并到XMM0的低端元素中,因此它对旧值有虚假的依赖。
addss
吞吐量(0.5个周期)而不是延迟(4个周期)的瓶颈。((您的CPU是Skylake的导数,因此是数字;较早的Intel使用专用的FP-add执行单元而不是在FMA单元上运行,具有3个周期的延迟,1个周期的吞吐量。https://agner.org/optimize/。可能是函数调用/ ret开销使您无法从运行中加法运算的延迟*带宽乘积中看到完整的8倍预期加速;如果您从单个函数中的循环中删除xorps
dep-breaking,您应该会得到它。)
[GCC往往对错误的依赖项非常“小心”
,花费额外的指令(前端带宽)来破坏它们,以防万一。在前端出现瓶颈的代码中(或总代码大小/ uop缓存占用量是一个因素),如果寄存器实际上已及时准备就绪,则这会降低性能。Clang / LLVM鲁less而轻率
,通常不会费心避免对当前函数中未写入的寄存器的错误依赖。 (即假设/假装寄存器在功能输入项上是“冷”的)。正如您在注释中所显示的那样,当在一个函数内部循环时,clang确实避免通过异或归零来创建循环承载的dep链,而不是通过多次调用同一函数。Clang甚至在某些情况下无缘无故地使用8位GP整数部分寄存器,而这与32位regs相比并没有节省任何代码大小或指令。通常情况可能很好,但是如果调用者(或同级函数调用)在执行此操作时仍然有高速缓存未命中负载,则存在耦合到长的dep链或创建循环承载的依赖链的风险例如。独立 dep链的更多信息。还相关:Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators)关于展开具有多个累加器的点积以隐藏FMA延迟。
[https://www.uops.info/html-instr/CVTSI2SS_XMM_R32.html具有此指令在不同架构上的性能详细信息。如果可以使用AVX和vcvtsi2ss %edi, %xmm7, %xmm0
],则可以避免这种情况(其中xmm7是您最近未写入的任何寄存器,或者是在导致EDI当前值的dep链中更早的寄存器) 。
此ISA设计缺陷得益于Intel在Pentium III上使用SSE1进行了短期优化。 P3在内部将128位寄存器分为两个64位一半。保留上半部分不变,让标量指令解码为单个uop。 (但这仍然使PIII零扩展的SSE2sqrtss
具有错误的依赖性)。最后,AVX至少在寄存器源(如果不是内存)中使用vsqrtsd %src,%src, %dst
可以避免这种情况,对于类似近视设计的标量int-> fp转换指令,可以类似地使用vcvtsi2sd %eax, %cold_reg, %dst
。(GCC错过优化报告:80586,89071,80571。)如果
cvtsi2ss
/sd
将寄存器的高位元素清零,我们将不会遇到这个愚蠢的问题,也不需要在周围散布Xor-Zeroing指令;谢谢英特尔。 (另一种策略是使用does
movd %eax, %xmm0
,然后对整个128位向量进行压缩的int-> fp转换。这对于int-> fp标量的float可能会收支平衡。转换为2 oups,向量策略为1 + 1。但在int-> fp压缩转换需要洗牌+ FP uop的情况下不能翻倍。)这正是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 CPU和Intel的虚假依赖,因为Haswell起)。