在 x86-64 中加载和存储长双打
Loading and storing long doubles in x86-64
今天我注意到一件奇怪的事。复制 long double
1 时,所有 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
需要四个微指令(none 融合)而 fstp
需要高达 7 uops(none 融合)与说每个 movaps
to/from 一个 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 的幂并且大小 .
对于需要复制 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 m80
和 fld m80
使用 2 个单独的存储数据(端口 4)或加载(p23)微指令,我认为我们可以假设它被分解为 64 位尾数和 16 位 sign:exponent.
当然,对于吞吐量和存储转发以外的情况下的延迟,movdqa
似乎是迄今为止最好的选择,因为正如您指出的那样,ABI 保证 16 字节对齐。一个 16 字节的存储可以转发到 fld m80
.
相同的参数适用于复制 double
或 float
整数与 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 复制 double
或 float
没问题。
32 位代码中的整数寄存器压力几乎总是很高,-mfpmath=sse
是 64 位代码的默认值。您可以想象在极少数情况下,使用 x87 在 64 位代码中复制 double
是值得的,但是如果编译器去寻找使用 x87 的地方,它们更有可能使事情变得更糟而不是更好。 gcc 有 -mfpmath=sse+387
,但通常不是很好。 (这甚至没有考虑使用 x87 + SSE 带来的物理寄存器文件压力。希望 "empty" x87 状态不使用任何物理寄存器。xsave
知道架构状态的一部分是空的,因此它可以避免保存它们...)
今天我注意到一件奇怪的事。复制 long double
1 时,所有 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
需要四个微指令(none 融合)而 fstp
需要高达 7 uops(none 融合)与说每个 movaps
to/from 一个 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 的幂并且大小
对于需要复制 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 m80
和 fld m80
使用 2 个单独的存储数据(端口 4)或加载(p23)微指令,我认为我们可以假设它被分解为 64 位尾数和 16 位 sign:exponent.
当然,对于吞吐量和存储转发以外的情况下的延迟,movdqa
似乎是迄今为止最好的选择,因为正如您指出的那样,ABI 保证 16 字节对齐。一个 16 字节的存储可以转发到 fld m80
.
相同的参数适用于复制 double
或 float
整数与 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 复制 double
或 float
没问题。
32 位代码中的整数寄存器压力几乎总是很高,-mfpmath=sse
是 64 位代码的默认值。您可以想象在极少数情况下,使用 x87 在 64 位代码中复制 double
是值得的,但是如果编译器去寻找使用 x87 的地方,它们更有可能使事情变得更糟而不是更好。 gcc 有 -mfpmath=sse+387
,但通常不是很好。 (这甚至没有考虑使用 x87 + SSE 带来的物理寄存器文件压力。希望 "empty" x87 状态不使用任何物理寄存器。xsave
知道架构状态的一部分是空的,因此它可以避免保存它们...)