如果没有 -fwrapv,GCC 将不会使用自己的优化技巧

Joh*_*nck 5 c++ optimization gcc x86-64 compiler-optimization

考虑这个 C++ 代码:

#include <cstdint>

// returns a if less than b or if b is INT32_MIN
int32_t special_min(int32_t a, int32_t b)
{
    return a < b || b == INT32_MIN ? a : b;
}
Run Code Online (Sandbox Code Playgroud)

GCC-fwrapv正确地认识到减去 1b可以消除特殊情况,并为 x86-64 生成以下代码

    lea     edx, [rsi-1]
    mov     eax, edi
    cmp     edi, edx
    cmovg   eax, esi
    ret
Run Code Online (Sandbox Code Playgroud)

但如果没有-fwrapv它,会生成更糟糕的代码:

    mov     eax, esi
    cmp     edi, esi
    jl      .L4
    cmp     esi, -2147483648
    je      .L4
    ret
.L4:
    mov     eax, edi
    ret
Run Code Online (Sandbox Code Playgroud)

我知道-fwrapv如果我编写依赖于签名溢出的 C++ 代码,这是需要的。但:

  1. 上面的 C++ 代码不依赖于有符号溢出(它是有效的标准 C++)。
  2. 我们都知道有符号溢出在 x86-64 上有特定的行为。
  3. 编译器知道它正在针对 x86-64 进行编译。

如果我编写了“手动优化”的 C++ 代码来尝试实现该优化,我知道这-fwrapv是必需的,否则编译器可能会确定有符号溢出是 UB 并在b == INT32_MIN. 但这里编译器处于控制之中,我看不出是什么阻止它使用没有-fwrapv. 有什么理由不允许吗?

Pet*_*des 4

这种错过的优化以前在 GCC 中发生过,比如没有完全将有符号 int add 视为关联,即使它正在编译带有包装加法的 2 的补码目标。所以它对无符号优化得更好。IIRC,原因是 GCC 失去了一些有关运营的信息,从而变得保守?我忘记了这个问题是否得到解决。

我找不到以前在 SO 上看到过的地方,以及 GCC 开发人员关于内部结构的回复;也许是在 GCC 错误报告中?我认为这是通过a+b+c+d+e(而不是)重新关联到依赖树来缩短关键路径之类的事情。但不幸的是它仍然存在于当前的 GCC 中:

int sum(int a, int b, int c, int d, int e, int f) {
    return a+b+c+d+e+f;
    // gcc and clang make one stupid dep chain
}

int sumv2(int a, int b, int c, int d, int e, int f) {
    return (a+b)+(c+d)+(e+f);
    // clang pessimizes this back to 1 chain, GCC doesn't
}

unsigned sumu(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) {
    return a+b+c+d+e+f;
    // gcc and clang make one stupid dep chain
}

unsigned sumuv2(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) {
    return (a+b)+(c+d)+(e+f);
    // GCC and clang pessimize back to 1 chain for unsigned
}
Run Code Online (Sandbox Code Playgroud)

Godbolt for x86-64 System V at-O3 clang 并gcc -fwrapv为所有 4 个函数制作相同的 asm,如您所料。

GCC(不带)为 和for生成-fwrapv相同的汇编(求和为,保存 的寄存器。) 但是 GCC 为和生成不同的汇编,因为它们使用带符号的sumusumuv2r8desumsumv2int

# gcc -O3 *without* -fwrapv
# The same order of order of operations as the C source
sum(int, int, int, int, int, int):
        add     edi, esi     # a += b
        add     edi, edx     # ((a+b) + c) ...
        add     edi, ecx     # sum everything into EDI
        add     edi, r8d
        lea     eax, [rdi+r9]
        ret

# also as written, the source order of operations:
sumv2(int, int, int, int, int, int):
        add     edi, esi    # a+=b
        add     edx, ecx    # c+=d
        add     r8d, r9d    # e+=f
        add     edi, edx       # a += c
        lea     eax, [rdi+r8]  # retval = a + e
        ret
Run Code Online (Sandbox Code Playgroud)

因此具有讽刺意味的是,当 GCC 不重新关联源时,它会生成更好的汇编。这是假设所有 6 个输入同时准备就绪。如果早期代码的无序执行仅在每个周期产生输入寄存器 1,则此处的最终结果将在最终输入准备好后仅 1 个周期准备就绪(假设最终输入为 )f

但如果最后一个输入是aor b,则结果要等到 5 个周期后才能准备好,并且尽可能使用像 GCC 和 clang 这样的单链。与树缩减的 3 个周期最坏情况相比,2 个周期最好情况(如果ef最后准备好)。

(更新:-mtune=znver2使 GCC 重新关联到一棵树中,谢谢@amonakov。所以这是一个默认的调整选择,对我来说似乎很奇怪,至少对于这个特定的问题大小。请参阅GCC 源代码,搜索reassoc以查看成本其他调整设置;其中大多数都是1,1,1,1疯狂的,特别是对于浮点。这可能就是为什么 GCC 在展开 FP 循环时未能使用多个向量累加器,从而达不到目的。)

int但无论如何,这是 GCC 仅与重新关联签名的情况-fwrapv 很明显,它对自己的限制超出了必要的限度,没有-fwrapv


相关:编译器优化可能会导致整数溢出。可以吗?- 这当然是合法的,不这样做就是错过优化。

GCC 并没有完全被签名所束缚int;它会自动矢量化int sum += arr[i],并且它确实能够优化为什么GCC不将a*a*a*a*a*a优化为(a*a*a)*(a*a*a)?为签署int a.