【问题标题】:Loading and storing long doubles in x86-64在 x86-64 中加载和存储长双精度数
【发布时间】:2018-05-12 11:18:21
【问题描述】:

我今天注意到一件奇怪的事情。复制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 需要四个微指令(未融合)而fstp 需要高达 七个 微指令(未融合)而不是说movapsxmm 寄存器之间的单个融合 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 相同的程序集,如之前针对 gccicc 所示,但 clang 现在使用:

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 的幂并且大小must usually be at least as large as the alignment

【问题讨论】:

  • 一个 10 字节的存储(我假设是 8 + 2)和一个 16 字节的重新加载会导致存储转发停止。除此之外,对于您不打算对其进行操作的情况,使用默认代码生成似乎纯粹是错过了优化。
  • 这让我想起了 atomic<double> 加载/存储的错过优化:即使不需要 CAS,也经常反弹到整数寄存器,只需 movstackoverflow.com/questions/45055402/…
  • 通过struct 的隧道有时会避免它,这很奇怪。似乎发生的事情是gcc 的“标量化”开始了,因此带有一个long double 的简单结构最终看起来像long double,然后返回到错误的代码生成(但不是clang) .当您添加足够多的其他东西时,它会停止并进入通常的结构复制逻辑,这要好得多。奇怪的是,icc 仍然可以处理更复杂的一些,比如this one。尝试删除或添加 int 成员,代码会完全改变。
  • 我认为您只是看到编译器知道如何复制整个结构,而不管内容如何。当编译器查看结构并将其“优化”为对单个原始类型执行的操作时,您将获得用于加载“结构”或“长双精度”的默认代码生成。只有long double 这特别糟糕。 (尽管使用 SSE2 而不是 x87 真正复制 double 也更好,即使使用 -mfpmath=387。没有实际的 ALU uop,但对于 fld/fstp,存储重新加载延迟比 @987654372 高 1c @/movq(来自 Agner Fog 的 SKL)
  • @peter 你检查了long double 加上 4 个ints 的奇怪 ICC 行为吗?

标签: x86 x86-64 compiler-optimization x87


【解决方案1】:

对于需要复制 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 加载/存储指令。我们知道 fstp m80fld m80 使用 2 个单独的存储数据(端口 4)或加载(p23)微指令,我认为我们可以假设它被分解为 64 位尾数和 16 位符号:指数。

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


同样的论点适用于使用整数复制 doublefloat 与 x87(例如 32 位代码)fld m32/fstp m32 具有高 1 个周期的往返在 Sandybridge 系列 CPU 上,延迟比 SSE movd 高 2 个周期,比整数 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 的物理寄存器文件压力。希望“空”x87 状态不使用任何物理寄存器。xsave 知道部分架构状态为空,因此可以避免保存它们。 ..)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-02
    • 2016-08-17
    相关资源
    最近更新 更多