【问题标题】:Why does the latency of the sqrtsd instruction change based on the input? Intel processors为什么 sqrtsd 指令的延迟会根据输入而变化?英特尔处理器
【发布时间】:2024-05-02 08:50:02
【问题描述】:

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;
}

【问题讨论】:

  • 向我们展示测试代码。我可以想象由编译器而不是处理器进行优化的实现。
  • 处理器并不聪明:它们执行给定的指令。
  • 你不是在想像:instlatx64 for skylake 还列出了 18(最坏情况)和 13(简单值)
  • 您的内联 asm 没有意义,无法编译:godbolt.org/z/rJA6nS"i" 指定是立即数,不能是输出约束。 sqrtsd 只接受 reg/mem 输入,而不是立即输入,因此即使编译也不会汇编。此外,使用 compile-time-constant immediates 不会让您测试延迟,只有吞吐量。但是你的数字看起来很正常,所以无论你实际做了什么,都可能测试了 sqrtsd 延迟。

标签: c performance assembly intel cpu-architecture


【解决方案1】:

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...)。所以非零位非常接近顶部,但显然这足以取消它的资格。

(+-InfNaN 也很快。普通的负数也是如此: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 错过优化报告:805868907180571。)


在许多早期的 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 的指令中提取的一些吞吐量/延迟数字表格。

【讨论】:

  • 顺便问一下,两个极端之间的延迟呢?它们会发生吗?我无法在我的 Haswell 上实现它,但这不是决定性的
  • @harold:IDK,我猜如果可能的话,尾数中的尾随零会减少一些。但对于最简单的情况,也许只有一种特殊情况的提前检测器。 Haswell 的下基数除法器应该使尽早寻找更有利可图,但也许这是一个初始估计(来自 rsqrt 使用的同一张表)是否准确的问题,如果不是,那么它需要迭代细化所有走到尽头。
  • rsqrt 并不完全适用于 2 的幂(无论如何在 Haswell 上),但到目前为止,我发现平方根速度很快的唯一输入是 2 和 0 rsqrt 指令似乎不仅仅是一个查找,因为它们的延迟实际上是多长时间
  • @harold:rsqrt 可能不是 LUT 的原始输出(是的,就像您编辑的那样,高延迟可能是一些工作)。或者也许它导致简单输入(全零尾数)的确切答案。或者,也许全零尾数可以跳过 LUT 查找开始细化。我对硬件分频器的了解还不够,无法排除这些猜测。 ://
  • sqrtsd 对于奇数指数的 2 次幂是否快?还是仅适用于具有偶数指数的 2 次幂?这很有趣。