考虑到这个函数,
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-math
的paddd
。
我从英特尔内部指南中获得了下表。
(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;
}