为什么 GCC 和 Clang 不优化乘以 2^n 的浮点数到指数的整数 PADDD,即使使用 -ffast-math?
Why don't GCC and Clang optimize multiplication by 2^n with a float to integer PADDD of the exponent, even with -ffast-math?
考虑到这个功能,
float mulHalf(float x) {
return x * 0.5f;
}
以下函数产生与 normal input/output.
相同的结果
float mulHalf_opt(float x) {
__m128i e = _mm_set1_epi32(-1 << 23);
__asm__ ("paddd\t%0, %1" : "+x"(x) : "xm"(e));
return x;
}
这是 -O3 -ffast-math
的汇编输出。
mulHalf:
mulss xmm0, DWORD PTR .LC0[rip]
ret
mulHalf_opt:
paddd xmm0, XMMWORD PTR .LC1[rip]
ret
-ffast-math
启用 -ffinite-math-only
,它“假设参数和结果不是 NaN 或 +-Infs”[1].
因此 mulHalf
的编译输出可能会更好地使用 paddd
和 -ffast-math
on 如果这样做会在 -ffast-math
.[=32= 的容差下产生更快的代码]
得到了以下 tables
(MULSS)
Architecture Latency Throughput (CPI)
Skylake 4 0.5
Broadwell 3 0.5
Haswell 5 0.5
Ivy Bridge 5 1
(PADDD)
Architecture Latency Throughput (CPI)
Skylake 1 0.33
Broadwell 1 0.5
Haswell 1 0.5
Ivy Bridge 1 0.5
显然,paddd
是一条更快的指令。然后我想也许是因为整数和浮点单元之间的旁路延迟。
This answer 显示来自 Agner Fog 的 table。
Processor Bypass delay, clock cycles
Intel Core 2 and earlier 1
Intel Nehalem 2
Intel Sandy Bridge and later 0-1
Intel Atom 0
AMD 2
VIA Nano 2-3
看到这一点,paddd
似乎仍然是赢家,尤其是在 Sandy Bridge 之后的 CPU 上,但是为最近的 CPU 指定 -march
只是将 mulss
更改为 vmulss
, 它有一个类似的 latency/throughput.
为什么即使使用 -ffast-math
,GCC 和 Clang 也不将 2^n 的浮点数乘法优化为 paddd
?
0.0f
的输入失败,-ffast-math
不排除。 (尽管从技术上讲这是次正规的特例,恰好也有零尾数。)
整数减法会换行到 all-ones 指数字段,并翻转符号位,所以你会得到 0.0f * 0.5f
产生 -Inf
,这是不可接受的。
@chtz 指出可以使用 psubusw
修复 +0.0f
的情况,但对于 -0.0f
-> +Inf
仍然失败。所以不幸的是,即使 -ffast-math
允许零的“错误”符号,这也不可用。但是即使 fast-math.
对于无穷大和 NaN 也是完全错误的也是不可取的
除此之外,是的,我认为这会奏效,并且在 Nehalem 以外的 CPU 上,在旁路延迟与 ALU 延迟之间为自己付出代价,即使在其他 FP 指令之间使用也是如此。
0.0 行为是个阻碍。除此之外,与其他输入的 FP 乘法相比,下溢行为更不理想,例如即使设置了 FTZ(输出清零)也会产生次正常值。使用 DAZ 集(非正规化为零)读取它的代码仍然可以正确处理它,但是 FP bit-pattern 对于具有最小归一化指数(编码为 1
)和 non-zero尾数。例如将标准化数字乘以 0.5f
.
,您可以获得 0x00000001
的 bit-pattern
即使不是 0.0f
showstopper,这种怪异现象也可能超出 GCC 愿意加诸于人们的范围。因此,即使对于 GCC 可以证明 non-zero 的情况,我也不期望它,除非它也可以证明远离 FLT_MIN。这可能非常罕见,不值得寻找。
当你知道它是安全的时候你当然可以手动完成它,尽管使用 SIMD 内在函数更方便。 我希望标量 type-punning 的 asm 相当糟糕,当您只需要低标量 FP 元素时,可能是整数 sub
附近的 2x movd
,而不是将其保存在 paddd
的 XMM 中。
Godbolt for several attempts,包括直接的内在函数,clang 会像我们希望的那样编译成 memory-source paddd
。 Clang 的 shuffle 优化器看到上面的元素是“死的”(_mm_cvtss_f32
只读取底部的元素),并且能够将它们视为“不关心”。
// clang compiles this fully efficiently
// others waste an instruction or more on _mm_set_ss to zero the upper XMM elements
float mulHalf_opt_intrinsics(float x) {
__m128i e = _mm_set1_epi32(-1u << 23);
__m128 vx = _mm_set_ss(x);
vx = _mm_castsi128_ps( _mm_add_epi32(_mm_castps_si128(vx), e) );
return _mm_cvtss_f32(vx);
}
还有一个普通的标量版本。我还没有测试它是否可以 auto-vectorize,但可以想象它可以这样做。否则,GCC 和 clang 都会执行 movd
/add
/movd
(或 sub
)将值反弹到 GP-integer 寄存器。
float mulHalf_opt_memcpy_scalar(float x) {
uint32_t xi;
memcpy(&xi, &x, sizeof(x));
xi += -1u << 23;
memcpy(&x, &xi, sizeof(x));
return x;
}
考虑到这个功能,
float mulHalf(float x) {
return x * 0.5f;
}
以下函数产生与 normal input/output.
相同的结果float mulHalf_opt(float x) {
__m128i e = _mm_set1_epi32(-1 << 23);
__asm__ ("paddd\t%0, %1" : "+x"(x) : "xm"(e));
return x;
}
这是 -O3 -ffast-math
的汇编输出。
mulHalf:
mulss xmm0, DWORD PTR .LC0[rip]
ret
mulHalf_opt:
paddd xmm0, XMMWORD PTR .LC1[rip]
ret
-ffast-math
启用 -ffinite-math-only
,它“假设参数和结果不是 NaN 或 +-Infs”[1].
因此 mulHalf
的编译输出可能会更好地使用 paddd
和 -ffast-math
on 如果这样做会在 -ffast-math
.[=32= 的容差下产生更快的代码]
(MULSS)
Architecture Latency Throughput (CPI)
Skylake 4 0.5
Broadwell 3 0.5
Haswell 5 0.5
Ivy Bridge 5 1
(PADDD)
Architecture Latency Throughput (CPI)
Skylake 1 0.33
Broadwell 1 0.5
Haswell 1 0.5
Ivy Bridge 1 0.5
显然,paddd
是一条更快的指令。然后我想也许是因为整数和浮点单元之间的旁路延迟。
This answer 显示来自 Agner Fog 的 table。
Processor Bypass delay, clock cycles
Intel Core 2 and earlier 1
Intel Nehalem 2
Intel Sandy Bridge and later 0-1
Intel Atom 0
AMD 2
VIA Nano 2-3
看到这一点,paddd
似乎仍然是赢家,尤其是在 Sandy Bridge 之后的 CPU 上,但是为最近的 CPU 指定 -march
只是将 mulss
更改为 vmulss
, 它有一个类似的 latency/throughput.
为什么即使使用 -ffast-math
,GCC 和 Clang 也不将 2^n 的浮点数乘法优化为 paddd
?
0.0f
的输入失败,-ffast-math
不排除。 (尽管从技术上讲这是次正规的特例,恰好也有零尾数。)
整数减法会换行到 all-ones 指数字段,并翻转符号位,所以你会得到 0.0f * 0.5f
产生 -Inf
,这是不可接受的。
@chtz 指出可以使用 psubusw
修复 +0.0f
的情况,但对于 -0.0f
-> +Inf
仍然失败。所以不幸的是,即使 -ffast-math
允许零的“错误”符号,这也不可用。但是即使 fast-math.
除此之外,是的,我认为这会奏效,并且在 Nehalem 以外的 CPU 上,在旁路延迟与 ALU 延迟之间为自己付出代价,即使在其他 FP 指令之间使用也是如此。
0.0 行为是个阻碍。除此之外,与其他输入的 FP 乘法相比,下溢行为更不理想,例如即使设置了 FTZ(输出清零)也会产生次正常值。使用 DAZ 集(非正规化为零)读取它的代码仍然可以正确处理它,但是 FP bit-pattern 对于具有最小归一化指数(编码为 1
)和 non-zero尾数。例如将标准化数字乘以 0.5f
.
0x00000001
的 bit-pattern
即使不是 0.0f
showstopper,这种怪异现象也可能超出 GCC 愿意加诸于人们的范围。因此,即使对于 GCC 可以证明 non-zero 的情况,我也不期望它,除非它也可以证明远离 FLT_MIN。这可能非常罕见,不值得寻找。
当你知道它是安全的时候你当然可以手动完成它,尽管使用 SIMD 内在函数更方便。 我希望标量 type-punning 的 asm 相当糟糕,当您只需要低标量 FP 元素时,可能是整数 sub
附近的 2x movd
,而不是将其保存在 paddd
的 XMM 中。
Godbolt for several attempts,包括直接的内在函数,clang 会像我们希望的那样编译成 memory-source paddd
。 Clang 的 shuffle 优化器看到上面的元素是“死的”(_mm_cvtss_f32
只读取底部的元素),并且能够将它们视为“不关心”。
// clang compiles this fully efficiently
// others waste an instruction or more on _mm_set_ss to zero the upper XMM elements
float mulHalf_opt_intrinsics(float x) {
__m128i e = _mm_set1_epi32(-1u << 23);
__m128 vx = _mm_set_ss(x);
vx = _mm_castsi128_ps( _mm_add_epi32(_mm_castps_si128(vx), e) );
return _mm_cvtss_f32(vx);
}
还有一个普通的标量版本。我还没有测试它是否可以 auto-vectorize,但可以想象它可以这样做。否则,GCC 和 clang 都会执行 movd
/add
/movd
(或 sub
)将值反弹到 GP-integer 寄存器。
float mulHalf_opt_memcpy_scalar(float x) {
uint32_t xi;
memcpy(&xi, &x, sizeof(x));
xi += -1u << 23;
memcpy(&x, &xi, sizeof(x));
return x;
}