c语言 - 为什么 GCC 和 Clang 不使用指数的浮点数到整数 PADDD 优化乘法 2^n,即使使用 -ffast-math?



考虑到这个函数,

float mulHalf(float x) {
return x * 0.5f;
}

以下函数产生与正常输入/输出相同的结果。

float mulHalf_opt(float x) {
__m128i e = _mm_set1_epi32(-1 << 23);
__asm__ ("padddt%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启用"假设参数和结果不是NaN或+-Infs"的-ffinite-math-only[1]。

因此,如果这样做在-ffast-math的容差下生成更快的代码,则mulHalf的编译输出可能更好地使用带有-ffast-mathpaddd

我从英特尔内部指南中获得了下表。

(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是一个更快的指令。然后我想可能是因为整数和浮点单元之间的旁路延迟。

这个答案显示了来自Agner Fog的表格。

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,它具有类似的延迟/吞吐量。

为什么 GCC 和 Clang 不优化乘法 2^n 与浮点数以paddd即使有-ffast-math

对于输入0.0f失败,-ffast-math不排除。 (尽管从技术上讲,这是亚常态的特例,恰好也有零尾数。

整数减法将换行到全一指数字段,并翻转符号位,因此您会0.0f * 0.5f产生-Inf,这是根本不可接受的。

@chtz指出,+0.0f的情况可以通过使用psubusw进行修复,但这仍然无法-0.0f->+Inf。 所以不幸的是,即使-ffast-math允许"错误"的零符号,这也无法使用。 但是,即使使用快速数学,对于无穷大和NaN来说完全错误也是不可取的。


除此之外,是的,我认为这会起作用,并且即使在其他 FP 指令之间使用,也会在 Nehalem 以外的 CPU 上为旁路延迟与 ALU 延迟付出代价。

0.0 的行为是一个引人注目的障碍。 除此之外,对于其他输入,下溢行为远不如FP乘法,例如,即使设置了FTZ(输出时齐平为零)也会产生次正态。 使用 DAZ 集读取它的代码(非正规为零)仍然可以正确处理它,但对于具有最小规范化指数(编码为1)和非零尾数的数字,FP 位模式也可能是错误的。 例如,通过将规范化数字乘以0.5f来,您可以得到0x00000001的位模式。

即使不是0.0f的表演,这种怪异也可能比GCC愿意对人们施加的更多。 因此,即使对于GCC可以证明非零的情况,我也不会期望它,除非它也证明与FLT_MIN相去甚远。 这可能很少见,不值得寻找。

当您知道它是安全的时,您当然可以手动执行此操作,尽管使用 SIMD 内部函数要方便得多。我希望标量类型双关语的asm相当糟糕,可能是整数sub的2倍movd,而不是在只需要低标量FP元素时将其保留在XMM中paddd

Godbolt 进行了几次尝试,包括简单的内联函数,它像我们希望的那样编译为一个内存源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);
}

还有一个普通的标量版本。 我还没有测试它是否可以自动矢量化,但它可能会这样做。否则,GCC 和 clang 都会执行movd/add/movd(或sub)将值反弹到 GP 整数寄存器。

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;
}

最新更新