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

py49o6xq  于 2023-01-14  发布在  其他
关注(0)|答案(1)|浏览(123)

考虑下面的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;
}

-fwrapv的GCC正确地认识到b减1可以消除特殊情况,它generates this code for x86-64

lea     edx, [rsi-1]
    mov     eax, edi
    cmp     edi, edx
    cmovg   eax, esi
    ret

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

mov     eax, esi
    cmp     edi, esi
    jl      .L4
    cmp     esi, -2147483648
    je      .L4
    ret
.L4:
    mov     eax, edi
    ret

我知道如果我编写依赖于有符号溢出的C代码,就需要-fwrapv。但是:
1.上面的C
代码不依赖于有符号溢出(它是有效的标准C++)。
1.我们都知道有符号溢出在x86-64上有特定的行为。
1.编译器知道它正在为x86-64编译。
如果我写了“手工优化”的C++代码来实现这种优化,我知道-fwrapv是必需的,否则编译器可能会决定有符号溢出是UB,并在b == INT32_MIN的情况下做任何它想做的事情。而且我看不出是什么阻止它使用没有-fwrapv的优化。是否有什么原因不允许它这样做?

b4lqfgs4

b4lqfgs41#

这种优化缺失以前在GCC中也发生过,比如没有完全把有符号的int add当作关联的,尽管它是用 Package 加法编译2的补码目标的。所以它对无符号的优化更好。IIRC,原因是像GCC丢失了一些关于操作的信息,因此是保守的?我忘了这个问题是否得到了修复。
我找不到我以前在SO上看到过的关于内部结构的GCC开发人员的回复;也许是在GCC的bug报告中?我想是像a+b+c+d+e(not)这样的东西重新关联到依赖树中以缩短关键路径。但不幸的是,它仍然存在于当前的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
}

Godbolt适用于x86-64 System V(-O3),clang和gcc -fwrapv为所有4个函数生成相同的asm,如您所料。

GCC(没有-fwrapv)为sumusumuv2生成相同的asm(求和到r8d,保存e的reg)。但是GCC为sumsumv2生成不同的asm,因为它们使用带符号的int

# 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

因此,具有讽刺意味的是,GCC在不重新关联源代码时使 * asm更好,这是假设所有6个输入同时就绪,如果前面代码的乱序执行每个周期只产生1个输入寄存器,那么这里的最终结果将在最终输入就绪后仅1个周期就绪,假设最终输入为f
但是,如果最后一个输入是ab,则结果将直到5个周期之后才准备好,其中在可能的情况下使用像GCC和clang这样的单链,而树约简的最坏情况是3个周期,最好情况是2个周期(如果ef最后准备好)。
(更新:-mtune=znver2使GCC重新关联到一个树中,谢谢@amonakov。所以这是一个默认的调优选择,我觉得很奇怪,至少对于这个特定的问题大小来说是这样。参见GCC源代码,搜索reassoc以查看其他调优设置的成本;大多数都是1,1,1,1,这很疯狂,尤其是对于浮点数。这可能是GCC在展开FP循环时无法使用多个向量累加器的原因,违背了目的。)

**但无论如何,这是GCC仅将带符号的int-fwrapv重新关联的情况。**因此,很明显,如果没有-fwrapv,它会限制自己。

相关:编译器优化可能会导致整数溢出。这样可以吗?-这当然是法律的的,如果不这样做就是错过了优化。
GCC并没有完全被带符号的int所束缚;它会自动向量化int sum += arr[i],并且它会设法优化为什么GCC不把a* a * a * aa优化为(aaa)(a* a * a)?对于有符号的int a

相关问题