为什么添加xorps指令会使此功能快5倍?

问题描述 投票:1回答:1

我当时正忙着使用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)。

clang x86-64 cpu-architecture sse microbenchmark
1个回答
4
投票

[cvtsi2ss %edi, %xmm0将float合并到XMM0的低端元素中,因此它对旧值有虚假的依赖。

异或归零会破坏dep链,让乱序的exec发挥其魔力。因此,您会遇到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链或创建循环承载的依赖链的风险例如。


请参阅Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths,以了解有关OoO执行程序如何重叠短至中等长度

独立 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链中更早的寄存器) 。

正如我在Why does the latency of the sqrtsd instruction change based on the input? Intel processors中提到的

此ISA设计缺陷得益于Intel在Pentium III上使用SSE1进行了短期优化。 P3在内部将128位寄存器分为两个64位一半。保留上半部分不变,让标量指令解码为单个uop。 (但这仍然使PIII sqrtss具有错误的依赖性)。最后,AVX至少在寄存器源(如果不是内存)中使用vsqrtsd %src,%src, %dst可以避免这种情况,对于类似近视设计的标量int-> fp转换指令,可以类似地使用vcvtsi2sd %eax, %cold_reg, %dst。(GCC错过优化报告:805868907180571。)

如果cvtsi2ss / sd将寄存器的高位元素清零,我们将不会遇到这个愚蠢的问题,也不需要在周围散布Xor-Zeroing指令;谢谢英特尔。 (另一种策略是使用

does

零扩展的SSE2 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起)。
© www.soinside.com 2019 - 2024. All rights reserved.