为什么 GCC 和 Clang 不使用指数的浮点到整数 PADDD 来优化乘以 2^n 的乘法,即使使用 -ffast-math 也是如此?

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 指定只需更改mulssvmulss,它具有类似的延迟/吞吐量。

为什么 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)