为什么 sqrtsd 指令的延迟会根据输入而变化?英特尔处理器

Why does the latency of the sqrtsd instruction change based on the input? Intel processors

Intel intrinsic guide 上说明名为 "sqrtsd" 的指令有 18 个周期的延迟。

我用自己的程序测试过,比如输入0.15是正确的。但是当我们取 256(或任何 2^x)数字时,延迟仅为 13。这是为什么呢?

我的一个理论是,因为 13 是 "sqrtss" 的延迟,它与 "sqrtsd" 相同,但在 32 位浮点上完成,那么也许处理器足够聪明,可以理解 256 可以适合 32 位,因此使用该版本,而 0.15 需要完整的 64 位,因为它不能以有限的方式表示。

我正在使用内联汇编,这里是用 gcc -O3 和 -fno-tree-vectorize 编译的相关部分。

static double sqrtsd (double x) {
    double r;
    __asm__ ("sqrtsd %1, %0" : "=x" (r) : "x" (x));
    return r;
}

SQRT* 和 DIV* 是仅有的两个 "simple" ALU 指令(单 uop,不是微码分支/循环)在现代 Intel/AMD 上具有数据相关的吞吐量或延迟CPU。 (不计算 add/multiply/fma 中非正规又称为次正规 FP 值的微代码辅助)。其他一切都几乎是固定的,因此无序 uop 调度机制不需要等待确认结果已经准备好某个周期,它只知道它会。

像往常一样,英特尔的内在函数指南给出了一个过于简化的性能图。对于 Skylake 上的双精度,实际延迟不是固定的 18 个周期。 (根据您选择引用的数字,我假设您有一个 Skylake。)

div/sqrt很难实现;即使在硬件方面,我们能做的最好的事情就是迭代优化过程。一次精炼更多位(自 Broadwell 以来的 radix-1024 分频器)加速它(参见 this Q&A about the hardware)。 但它仍然足够慢,以至于使用早期输出来加速简单的情况(或者也许加速机制只是跳过现代 CPU 上全零尾数的设置步骤,部分 -流水线 div/sqrt 单元。较旧的 CPU 吞吐量 = FP div/sqrt 的延迟;该执行单元更难流水线。)


https://www.uops.info/html-instr/VSQRTSD_XMM_XMM_XMM.html shows Skylake SQRTSD can vary from 13 to 19 cycle latency. The SKL (client) numbers only show 13 cycle latency, but we can see from the detailed SKL vsqrtsd page that they only tested with input = 0. SKX (server) numbers show 13-19 cycle latency. (This page has the detailed breakdown of the test code they used, including the binary bit-patterns for the tests.) Similar testing (with only 0 for client cores) was done on the non-VEX sqrtsd xmm, xmm 页。 :/

InstLatx64 结果显示 Skylake-X(使用与 Skylake-client 相同的内核,但启用了 AVX512)上的最佳/最差情况延迟为 13 到 18 个周期。

Agner Fog's instruction tables 在 Skylake 上显示 15-16 周期延迟。 (Agner 通常会使用一系列不同的输入值进行测试。)他的测试自动化程度较低,有时与其他结果不完全匹配。

是什么让某些情况变得快速?

请注意,大多数 ISA(包括 x86)使用 binary floating point:
这些位将值表示为线性有效数(又名尾数)乘以 2exp 和一个符号位。

现代英特尔似乎只有两种速度(至少自 Haswell 以来)(请参阅评论中与@harold 的讨论。)例如即使是 2 的幂也都很快,例如 0.25、1、4 和 16。这些具有代表 1.0 的平凡尾数 = 0x0。 https://www.h-schmidt.net/FloatConverter/IEEE754.html 有一个很好的交互式十进制 <-> 单精度位模式转换器,带有用于设置位的复选框以及尾数和指数表示的注释。

在 Skylake 上,我在快速检查中发现的唯一快速情况是 even 2 的幂,例如 4.0 而不是 2.0。这些数字具有精确的 sqrt 结果,输入和输出都具有 1.0 尾数(仅隐含的 1 位集)。 9.0 并不快,即使它是完全可表示的,3.0 结果也是如此。 3.0 的尾数 = 1.5,只有尾数集的最高有效位在二进制表示中。 9.0 的尾数是 1.125 (0b00100...)。所以非零位非常接近顶部,但显然这足以取消它的资格。

(+-InfNaN 也很快。普通负数也是如此:结果 = -NaN。我测量了这些的 13 个周期延迟在 i7-6700k 上,与 4.0 相同。相对于慢速情况下的 18 周期延迟。)

x = sqrt(x) 肯定比 x = 1.0 快(除隐式前导 1 位外的全零尾数)。它有一个简单的输入和简单的输出。

对于 2.0,输入也很简单(全零尾数和更高的指数 1)但输出不是整数。 sqrt(2) 是无理数,因此在任何基数中都有无限的非零位。这显然会使它在 Skylake 上变慢。

Agner Fog's instruction tables 说 AMD K10 的整数 div 指令性能取决于 dividend (输入)中的有效位数,而不是商,但是搜索 Agner 的 microarch pdf 和指令表没有找到任何关于 sqrt 具体如何依赖于数据的脚注或信息。

在 FP sqrt 更慢的旧 CPU 上,速度范围可能有更多空间。我 认为 输入 的尾数中的有效位数可能是相关的。如果这是正确的,更少的有效位(尾随的尾随零)会更快。但是同样,在 Haswell/Skylake 上,唯一快速的案例似乎是 2 的偶数次幂。


你可以用一些东西来测试这个,它可以在不破坏数据依赖性的情况下将输出耦合回输入,例如andps xmm0, xmm1 / orps xmm0, xmm2 在 xmm0 中设置一个取决于 sqrtsd 输出的固定值。

或者更简单的测试延迟的方法是取 "advantage" 的 sqrtsd xmm0, xmm1 的错误输出依赖 - 它和 sqrtss leave the upper 64 / 32 bits (respectively) of the destination unmodified, thus the output register is also an input for that merging. I assume this is how your naive inline-asm attempt ended up bottlenecking on latency instead of throughput with the compiler picking a different register for the output so it could just re-read the same input in a loop. The inline asm you added to your question is totally broken and won't even compile, but perhaps your real code used "x" (xmm register) input and output constraints "i"(立即数)?

This NASM source 用于静态可执行测试循环(到 perf stat 下的 运行)使用 sqrtsd.[=42 的非 VEX 编码的虚假依赖=]

此 ISA 设计缺陷归功于英特尔在奔腾 III 上使用 SSE1 进行短期优化。 P3 在内部将 128 位寄存器处理为两个 64 位的一半。保留上半部分不变,让标量指令解码为单个 uop。 (但这仍然给 PIII sqrtss 一个错误的依赖)。 AVX 最终让我们通过 vsqrtsd dst, src,src 至少对寄存器源避免了这种情况,同样地 vcvtsi2sd dst, cold_reg, eax 对于类似近视设计的标量 int->fp 转换指令。 (GCC 优化失败报告:80586, 89071, 80571。)


在许多早期的 CPU 上,甚至吞吐量也是可变的,但 Skylake 充分加强了除法器,调度程序总是知道它可以在最后一个单精度输入后的 3 个周期开始一个新的 div/sqrt uop。

即使是 Skylake 双精度吞吐量也是可变的:如果 Agner Fog's instruction tables 是正确的,则在最后一个双精度输入 uop 之后 4 到 6 个周期。 https://uops.info/ shows a flat 6c reciprocal throughput. (Or twice that long for 256-bit vectors; 128-bit and scalar can use separate halves of the wide SIMD dividers for more throughput but the same latency.) See also Floating point division vs floating point multiplication 从 Agner Fog 的指令表中提取的一些 throughput/latency 数字。