SQRT* 和 DIV* 是仅有的两个“简单”ALU 指令(单 uop,不是微编码分支/循环),它们在现代 Intel/AMD CPU 上具有数据相关的吞吐量或延迟。 (不计算加法/乘法/fma 中非正规又名次正规 FP 值的微码辅助)。其他一切都已基本修复,因此无序的 uop 调度机制不需要等待某个周期确认结果已准备好,它只知道它会准备好。
像往常一样,英特尔的内在函数指南给出了过于简化的性能图。 Skylake 上双精度的实际延迟不是固定的 18 个周期。 (根据您选择引用的数字,我假设您有一个 Skylake。)
div/sqrt 难以实现;即使在硬件中,我们能做的最好的事情就是迭代细化过程。一次提炼更多位(自 Broadwell 以来的基数 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 显示 Skylake SQRTSD 可以在 13 到 19 个周期延迟之间变化。 SKL(客户端)数字仅显示 13 个周期延迟,但我们可以从详细的SKL vsqrtsd page 中看到他们仅在输入 = 0 的情况下进行测试。SKX(服务器)数字显示 13-19 个周期延迟。 (This page 有他们使用的测试代码的详细分类,包括测试的二进制位模式。)在the non-VEX sqrtsd xmm, xmm 页面上进行了类似的测试(客户端内核只有 0)。 :/
InstLatx64 结果显示 Skylake-X(使用与 Skylake-client 相同的内核,但启用了 AVX512)上的最佳/最差情况延迟为 13 到 18 个周期。
Agner Fog's instruction tables 在 Skylake 上显示 15-16 周期延迟。 (Agner 通常会使用一系列不同的输入值进行测试。)他的测试自动化程度较低,有时与其他结果不完全匹配。
是什么让某些案例变得更快?
请注意,大多数 ISA(包括 x86)使用 binary floating point:
这些位将值表示为线性有效数(又名尾数)乘以 2exp 和符号位。
现代英特尔似乎只有 2 种速度(至少从 Haswell 开始)(参见 cmets 中与 @harold 的讨论。)例如甚至 2 的幂都很快,例如 0.25、1、4 和 16。它们的尾数 = 0x0 代表 1.0。 https://www.h-schmidt.net/FloatConverter/IEEE754.html 有一个很好的交互式小数 单精度位模式转换器,带有用于设置位的复选框以及尾数和指数表示的注释。
在 Skylake 上,我在快速检查中发现的唯一快速案例是 偶数 2 的幂,例如 4.0,但不是 2.0。这些数字具有精确的 sqrt 结果,输入和输出都具有 1.0 尾数(仅设置了隐式 1 位)。 9.0 并不快,尽管它可以精确表示,3.0 结果也是如此。 3.0 的尾数 = 1.5,只有在二进制表示中设置的尾数的最高有效位。 9.0 的尾数是 1.125 (0b00100...)。所以非零位非常接近顶部,但显然这足以取消它的资格。
(+-Inf 和 NaN 也很快。普通的负数也是如此:result = -NaN。我在 i7-6700k 上测量了 13 个周期的延迟,与 @ 相同987654345@. 与慢速情况下的 18 个周期延迟相比。)
x = sqrt(x) 使用x = 1.0 肯定很快(除隐式前导 1 位外,尾数全为零)。它具有简单的输入和简单的输出。
对于 2.0,输入也很简单(尾数全为零,指数高 1),但输出不是整数。 sqrt(2) 是无理数,因此在任何基数中都有无限的非零位。这显然会使其在 Skylake 上运行缓慢。
Agner Fog's instruction tables 说 AMD K10 的整数 div 指令性能取决于股息(输入)中的有效位数,而不是商,但搜索 Agner 的 microarch pdf 和指令表并没有'找不到任何关于 sqrt 具体如何依赖数据的脚注或信息。
在具有更慢 FP sqrt 的较旧 CPU 上,可能有更多空间用于各种速度。我认为 输入 尾数中的有效位数可能是相关的。如果这是正确的,更少的有效位(有效数字中更多的尾随零)使其更快。但同样,在 Haswell/Skylake 上,唯一的快速案例似乎是 2 的幂次方。
您可以使用将输出耦合回输入而不破坏数据依赖性的东西来测试这个,例如andps xmm0, xmm1 / orps xmm0, xmm2 在 xmm0 中设置一个取决于 sqrtsd 输出的固定值。
或者测试延迟的更简单方法是利用 sqrtsd xmm0, xmm1 的错误输出依赖性的“优势” - 它和 sqrtss 分别保留了高 64 / 32 位目的地未修改,因此输出寄存器也是该合并的输入。 我认为这就是您天真的 inline-asm 尝试最终导致延迟而不是吞吐量瓶颈的原因,编译器为输出选择了不同的寄存器,因此它可以在循环中重新读取相同的输入。您添加到问题中的内联汇编已完全损坏,甚至无法编译,但也许您的真实代码使用"x"(xmm 寄存器)输入和输出constraints 而不是"i"(立即)?
This NASM source 用于静态可执行测试循环(在perf stat 下运行)使用带有sqrtsd 的非VEX 编码的错误依赖项。
这个 ISA 设计缺陷要归功于英特尔在 Pentium 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/ 显示平坦的 6c 倒数吞吐量。 (或者 256 位向量的两倍长;128 位和标量可以使用宽 SIMD 分频器的单独一半以获得更高的吞吐量但相同的延迟。)另请参阅 Floating point division vs floating point multiplication 以获取从 Agner Fog 的指令中提取的一些吞吐量/延迟数字表格。