【发布时间】:2018-05-12 11:18:21
【问题描述】:
我今天注意到一件奇怪的事情。复制long double1 时,所有gcc、clang 和icc 都会生成fld 和fstp 指令,并带有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 需要高达 七个 微指令(未融合)而不是说movaps 与 xmm 寄存器之间的单个融合 uop。
有趣的是,只要将long double 放入struct,clang 就会开始使用movaps。以下代码:
struct long_double {
long double x;
};
void copy_ld(long_double *dst, long_double *src) {
*src = *dst;
}
Compiles 到与 fld/fstp 相同的程序集,如之前针对 gcc 和 icc 所示,但 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
是否有任何功能性原因需要使用 fld 和 fstp 复制浮点值,或者只是错过了优化?
1 虽然long double(即x86 扩展精度浮点数)在x86 上名义上是10 个字节,但它具有sizeof == 16 和alignof == 16,因为对齐必须是2 的幂并且大小must usually be at least as large as the alignment。
【问题讨论】:
-
一个 10 字节的存储(我假设是 8 + 2)和一个 16 字节的重新加载会导致存储转发停止。除此之外,对于您不打算对其进行操作的情况,使用默认代码生成似乎纯粹是错过了优化。
-
这让我想起了
atomic<double>加载/存储的错过优化:即使不需要 CAS,也经常反弹到整数寄存器,只需mov。 stackoverflow.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