xiv*_*r77 5 c floating-point x86 assembly compiler-optimization
考虑到这个功能,
float mulHalf(float x) {
return x * 0.5f;
}
Run Code Online (Sandbox Code Playgroud)
以下函数与正常输入/输出产生相同的结果。
float mulHalf_opt(float x) {
__m128i e = _mm_set1_epi32(-1 << 23);
__asm__ ("paddd\t%0, %1" : "+x"(x) : "xm"(e));
return x;
}
Run Code Online (Sandbox Code Playgroud)
这是带有 的汇编输出-O3 -ffast-math
。
mulHalf:
mulss xmm0, DWORD PTR .LC0[rip]
ret
mulHalf_opt:
paddd xmm0, XMMWORD PTR .LC1[rip]
ret
Run Code Online (Sandbox Code Playgroud)
-ffast-math
启用-ffinite-math-only
“假设参数和结果不是 NaN 或 +-Infs” [1]。
因此,如果在 的容差下生成更快的代码,则的编译输出可能会更好地与onmulHalf
一起使用。paddd
-ffast-math
-ffast-math
我从Intel Intrinsics Guide中获得了下表。
(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
Run Code Online (Sandbox Code Playgroud)
显然,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
Run Code Online (Sandbox Code Playgroud)
看到这一点,paddd
似乎仍然是赢家,特别是在 Sandy Bridge 之后的 CPU 上,但-march
为最近的 CPU 指定只需更改mulss
为vmulss
,它具有类似的延迟/吞吐量。
为什么 GCC 和 Clang 不优化 2^n 与 float 的乘法以paddd
等于-ffast-math
?
Pet*_*des 11
对于 的输入,这会失败0.0f
,这-ffast-math
并不排除。(尽管从技术上讲,这是次正规的特殊情况,恰好也有一个零尾数。)。
整数减法将换行为全一指数字段,并翻转符号位,因此您将得到0.0f * 0.5f
,-Inf
这是根本不可接受的。
@chtz 指出+0.0f
可以使用 修复此情况,但对于->psubusw
仍然失败。不幸的是,即使允许“错误”的零符号,这也是不可用的。但即使对于快速数学来说,无穷大和 NaN 完全错误也是不可取的。-0.0f
+Inf
-ffast-math
除此之外,是的,我认为这会起作用,并且在除 Nehalem 之外的 CPU 上的旁路延迟与 ALU 延迟之间付出代价,即使在其他 FP 指令之间使用也是如此。
0.0 行为是一个令人震惊的行为。除此之外,下溢行为比其他输入的 FP 乘法要差得多,例如,即使设置了 FTZ(输出刷新为零)也会产生次正常。使用 DAZ 集(非正规数为零)读取它的代码仍然可以正确处理它,但是对于具有最小标准化指数(编码为1
)和非零尾数的数字,FP 位模式也可能是错误的。0x00000001
例如,您可以将标准化数字乘以得到位模式0.5f
。
即使不是为了0.0f
引人注目,这种怪异可能也超出了海湾合作委员会愿意强加给人们的程度。因此,即使 GCC 可以证明非零,我也不会期望它,除非它也可以证明远离 FLT_MIN。这种情况可能非常罕见,不值得寻找。
当您知道安全时,您当然可以手动执行此操作,尽管使用 SIMD 内在函数要方便得多。 我预计标量类型双关会产生相当糟糕的 asm,可能movd
是整数的2 倍sub
,而不是当paddd
您只需要低标量 FP 元素时将其保留在 XMM 中。
Godbolt 进行了多次尝试,包括简单的内在函数,它像我们希望的那样编译为内存源paddd
。Clang 的随机优化器发现上面的元素是“死的”(_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);
}
Run Code Online (Sandbox Code Playgroud)
还有一个简单的标量版本。我还没有测试它是否可以自动矢量化,但可以想象它可能会这样做。如果没有这个,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;
}
Run Code Online (Sandbox Code Playgroud)