Clang 为什么添加xorps指令会使使用cvtsi2ss和addss的函数速度提高5倍?

Clang 为什么添加xorps指令会使使用cvtsi2ss和addss的函数速度提高5倍?,clang,x86-64,cpu-architecture,sse,microbenchmark,Clang,X86 64,Cpu Architecture,Sse,Microbenchmark,我在使用谷歌基准测试优化函数时遇到了麻烦,在某些情况下,我的代码出人意料地慢了下来。我开始对它进行实验,查看编译后的程序集,最终提出了一个展示该问题的最小测试用例。下面是我提出的展示这种减速的组件: .text test: #xorps %xmm0, %xmm0 cvtsi2ss %edi, %xmm0 addss %xmm0, %xmm0 addss %xmm0, %xmm0 addss %xmm0, %xmm0 ad

我在使用谷歌基准测试优化函数时遇到了麻烦,在某些情况下,我的代码出人意料地慢了下来。我开始对它进行实验,查看编译后的程序集,最终提出了一个展示该问题的最小测试用例。下面是我提出的展示这种减速的组件:

    .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”浮点测试(int)注意注释掉的
xorps
指令。取消对该指令的注释可以显著提高函数的性能。使用我的i7-8700K机器进行测试,Google benchmark显示不带
xorps
指令的函数需要8.54ns(CPU),而带
xorps
指令的函数需要1.48ns。我已经在多台具有不同操作系统、处理器、处理器代和不同处理器制造商(英特尔和AMD)的计算机上进行了测试,它们都表现出类似的性能差异。重复
addss
指令会使减速更加明显(在某种程度上),并且这种减速仍然会使用此处的其他指令(例如
mulss
)甚至混合使用指令,只要它们在某种程度上都依赖于
%xmm0
中的值。值得指出的是,仅调用
xorps
每个函数调用就可以提高性能。使用循环外的
xorps
调用对性能进行采样(正如谷歌基准测试所做的那样),仍然显示了较慢的性能


由于在这种情况下,专门添加指令可以提高性能,因此这似乎是由于CPU中的某些低级别问题造成的。因为它发生在各种各样的CPU上,所以看起来这一定是有意的。然而,我找不到任何文档来解释为什么会发生这种情况。有人能解释这里发生了什么吗?这个问题似乎取决于复杂的因素,因为我在原始代码中看到的减速只发生在特定的优化级别(-O2,有时是-O1,但不是-Os),没有内联,也没有使用特定的编译器(Clang,但不是GCC)。

cvtsi2ss%edi,%xmm0
将float合并到xmm0的low元素中,因此它对旧值具有错误的依赖关系。
(通过重复调用同一函数,创建一个长循环携带的依赖关系链。)

xor归零打破了dep链,允许无序exec发挥其魔力。因此,您的瓶颈是
addss
吞吐量(0.5个周期),而不是延迟(4个周期)

你的CPU是Skylake的衍生物,所以这些是数字;早期Intel使用专用的FP add执行单元,而不是在FMA单元上运行,具有3个周期的延迟和1个周期的吞吐量。功能调用/ret开销可能会使您无法看到流水线FMA单元中8个飞行中
addss
uop的延迟*带宽乘积的8倍预期加速;如果在单个函数中从循环中删除
xorps
dep中断,您应该会得到加速


GCC倾向于非常“小心”错误的依赖关系,花费额外的指令(前端带宽)来打破它们以防万一。在前端出现瓶颈的代码中(或者在总代码大小/uop缓存占用是一个因素的情况下),如果寄存器实际上已经及时准备好,那么这会降低性能

Clang/LLVM在这方面是鲁莽和傲慢的,通常不会费心避免对当前函数中未写入的寄存器的错误依赖。(即,假设/假装寄存器在函数输入时为“冷”)。正如您在注释中所示,当在一个函数内循环时,clang确实避免了通过xor归零创建循环携带的dep链,而不是通过对同一个函数的多个调用

在某些情况下,与32位寄存器相比,Clang甚至无缘无故地使用8位GP整数部分寄存器,这不会节省任何代码大小或指令。通常情况下,这可能没什么问题,但如果调用方(或同级函数调用)在调用时仍有缓存未命中加载到该reg,则存在耦合到长dep链或创建循环承载依赖链的风险


有关OoO exec如何重叠短到中等长度的独立dep链的更多信息,请参阅。同样相关:是关于展开具有多个累加器的点积以隐藏FMA延迟

在各种UARCHE中具有此指令的性能详细信息


如果您可以使用AVX和
vcvtsi2ss%edi、%xmm7、%xmm0
(其中xmm7是您最近没有写入的任何寄存器,或者是导致edi当前值的dep链中较早的寄存器),则可以避免这种情况

正如我在书中提到的

这一ISA设计缺陷要归功于英特尔在奔腾III上对SSE1进行短期优化。P3将128位寄存器作为两个64位寄存器进行内部处理。不修改上半部分,让标量指令解码为单个uop。(但这仍然给了PIII
sqrtss
一个错误的依赖项)。AVX最终让我们通过
vsqrtsd%src,%src,%dst
避免了这种情况,至少对于寄存器源(如果不是内存),对于类似的近视设计的标量int->fp转换指令,
vcvtsi2sd%eax,%coldreg,%dst

(GCC遗漏的优化报告:,)

如果
cvtsi2ss
/
sd
已经将寄存器的上部元素归零,我们就不会有这个愚蠢的问题/就不需要到处散布异或归零指令;谢谢英特尔。(另一种策略是使用SSE2
movd%eax,%xmm0
进行零扩展,然后压缩int->f