在 x86-64 中加载和存储长双打

Loading and storing long doubles in x86-64

今天我注意到一件奇怪的事。复制 long double1 时,所有 gccclangicc 生成 fldfstp指令,具有 TBYTE 个内存操作数。

即如下函数:

void copy_prim(long double *dst, long double *src) {
    *src = *dst;
}

Generates 以下程序集:

copy_prim(long double*, long double*):
  fld TBYTE PTR [rdi]
  fstp TBYTE PTR [rsi]
  ret

现在根据 Agner's tables 这对性能来说是一个糟糕的选择,因为 fld 需要四个微指令(none 融合)而 fstp 需要高达 7 uops(none 融合)与说每个 movaps to/from 一个 xmm 寄存器的单个融合 uop。

有趣的是,一旦您将 long double 放入 structclang 就开始使用 movaps。以下代码:

struct long_double {
    long double x;
};

void copy_ld(long_double *dst, long_double *src) {
    *src = *dst;
}

Compiles 到与 fld/fstp 相同的程序集,如之前显示的 gcciccclang 现在使用:

copy_ld(long_double*, long_double*):
  movaps xmm0, xmmword ptr [rdi]
  movaps xmmword ptr [rsi], xmm0
  ret

奇怪的是,如果你将额外的 int 成员填充到 struct 中(由于对齐 双倍 其大小为 32 字节),所有编译器生成仅限 SSE 的复制代码:

copy_ldi(long_double_int*, long_double_int*):
  movdqa xmm0, XMMWORD PTR [rdi]
  movaps XMMWORD PTR [rsi], xmm0
  movdqa xmm0, XMMWORD PTR [rdi+16]
  movaps XMMWORD PTR [rsi+16], xmm0
  ret

使用 fldfstp 复制浮点值是否有任何功能性原因,或者只是错过了优化?


1 虽然 long double(即 x86 扩展精度浮点数)在 x86 上名义上是 10 个字节,但它有 sizeof == 16alignof == 16 因为对齐必须是 2 的幂并且大小 .

对于需要复制 long double 而不进行处理的代码,这看起来像是一个很大的优化失误。 fstp m80/fld m80 Skylake 上的往返延迟为 8 个周期,而 movdqa 从存储到重新加载的存储转发为 5 个周期。更重要的是,Agner 将 fstp m80 列为每 5 个时钟吞吐量一个,因此存在非流水线操作!

我能想到的唯一可能的好处是从仍在飞行的 long double 商店转发商店。考虑一个涉及一些 x87 数学的数据依赖链,一个 long double 存储,然后是你的函数,然后是一个 long double 加载和更多 x87 数学。根据 Agner 的表格,fld/fstp 将增加 8 个周期,但 movdqa 将看到存储转发停顿并为慢速存储转发添加 5 + 11 个左右的周期.

复制 m80 的最低延迟策略可能是 64 位 + 16 位整数 mov/movzx load/store 指令。我们知道 fstp m80fld m80 使用 2 个单独的存储数据(端口 4)或加载(p23)微指令,我认为我们可以假设它被分解为 64 位尾数和 16 位 sign:exponent.

当然,对于吞吐量和存储转发以外的情况下的延迟,movdqa 似乎是迄今为止最好的选择,因为正如您指出的那样,ABI 保证 16 字节对齐。一个 16 字节的存储可以转发到 fld m80.


相同的参数适用于复制 doublefloat 整数与 x87(例如 32 位代码)fld m32 /fstp m32 的往返延迟比 SSE movd 高 1 个周期,比 Sandybridge 系列 CPU 上的整数 mov 高 2 个周期。 (与 PowerPC / Cell load-hit-store 不同,从 FP 存储到整数加载的存储转发没有惩罚。x86 强大的内存排序模型不允许 FP 与整数的单独存储缓冲区,如果那是 PPC 所做的。)

一旦编译器意识到它不会在 float / double / long double 上使用任何 FP 指令,它通常应该用非-x87。但是如果整数/SSE 寄存器压力是个问题,用 x87 复制 doublefloat 没问题。

32 位代码中的整数寄存器压力几乎总是很高,-mfpmath=sse 是 64 位代码的默认值。您可以想象在极少数情况下,使用 x87 在 64 位代码中复制 double 是值得的,但是如果编译器去寻找使用 x87 的地方,它们更有可能使事情变得更糟而不是更好。 gcc 有 -mfpmath=sse+387,但通常不是很好。 (这甚至没有考虑使用 x87 + SSE 带来的物理寄存器文件压力。希望 "empty" x87 状态不使用任何物理寄存器。xsave 知道架构状态的一部分是空的,因此它可以避免保存它们...)