【问题标题】:What optimizations does __builtin_unreachable facilitate?__builtin_unreachable 促进了哪些优化?
【发布时间】:2019-07-12 20:32:16
【问题描述】:

从 gcc 的文档来看

如果控制流到达__builtin_unreachable的点,则程序未定义。

我认为__builtin_unreachable 可以以各种创造性的方式用作优化器的提示。所以我做了一个小实验

void stdswap(int& x, int& y)
{
    std::swap(x, y);
}

void brswap(int& x, int& y)
{
    if(&x == &y)
        __builtin_unreachable();
    x ^= y;
    y ^= x;
    x ^= y;
}

void rswap(int& __restrict x, int& __restrict y)
{
    x ^= y;
    y ^= x;
    x ^= y;
}

gets compiled to (g++ -O2)

stdswap(int&, int&):
        mov     eax, DWORD PTR [rdi]
        mov     edx, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], edx
        mov     DWORD PTR [rsi], eax
        ret
brswap(int&, int&):
        mov     eax, DWORD PTR [rdi]
        xor     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], eax
        xor     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rsi], eax
        xor     DWORD PTR [rdi], eax
        ret
rswap(int&, int&):
        mov     eax, DWORD PTR [rsi]
        mov     edx, DWORD PTR [rdi]
        mov     DWORD PTR [rdi], eax
        mov     DWORD PTR [rsi], edx
        ret

我假设stdswaprswap 从优化器的角度来看是最佳的。为什么brswap 不被编译成同样的东西?我可以用__builtin_unreachable 编译成同样的东西吗?

【问题讨论】:

  • stdswaprswap 的程序集实际上可以正常工作,即使 xy 别名也是如此。如果使用局部变量(即int temp = y; y = x; x = temp;)正常进行交换,GCC 也会愉快地发出与这些函数相同的指令序列——如今,“XOR 交换”在大多数情况下是一种反模式。
  • @ArneVogel 这就是重点,XOR 交换在别名时具有不同的行为,因此编译器可能不会发出与stdswap 相同的指令。我特地选择了一个反模式,以便验证__builtin_unreachable的效果。
  • 嗯,我的主要观点是相关性不等于因果关系。我猜想来自rswap 的异或交换优化真的很古老——几乎和过去的稀缺寄存器和相当愚蠢的编译器一样古老——而且 GCC 开发人员后来从未特别关心改进它。许多编译器优化基本上都是启发式算法之上的特殊情况,这两者都必须有人实现,并希望在少数被认为有意义的参考应用程序中展示其好处。

标签: c++ gcc optimization


【解决方案1】:

我认为您正在尝试对代码进行微优化,但方向错误。

__builtin_unreachable 以及 __builtin_expect 执行预期的操作 - 在您的情况下,从未使用的 if 运算符中删除不必要的 cmpjnz

编译器应使用您编写的 C 代码生成机器代码,以生成可预测的程序。并且在优化过程中,当它知道优化算法时,它能够找到并优化(即用更好的机器代码版本替换)一些模式——这样的优化不会破坏程序的行为。

例如像

char a[100];
for(int i=0; i < 100; i++)
   a[i]  = 0;

将被替换为使用汇编实现的对库 std::memset(a,0,100) 的单次调用,并且对于当前的 CPU 架构是最佳的。

以及能够检测到的编译器

x ^= y;
y ^= x;
x ^= y;

并用最简单的 mashie 代码替换它。

我认为您的 if 运算符和 unreached 指令影响了编译器优化器,因此无法优化。

在交换两个整数的情况下,第三个临时交换变量可以由编译器自行删除,即它将类似于

movl    $2, %ebx
movl    $1, %eax
xchg    %eax,%ebx  

其中 ebx 和 eax 寄存器值实际上是您的 x 和 y。你可以自己实现它

void swap_x86(int& x, int& y)
{
    __asm__ __volatile__( "xchg %%rax, %%rbx": "=a"(x), "=b"(y) : "a"(x), "b"(y) : );
}
...
int a = 1;
int b = 2;
swap_x86(a,b);

何时使用 __builtin_unreachable?可能当您知道某些情况实际上是不可能的,但从逻辑上讲它可能会发生。 IE。你有一些像

这样的功能
void foo(int v) {

    switch( v ) {
        case 0:
            break;
        case 1:
            break;
        case 2:
            break;
        case 3:
            break;
        default:
            __builtin_unreachable();
    }
}

您知道v 参数值始终介于 0 和 3 之间。但是,int 范围是 -21474836482147483647(当 int 是 32 位类型时),编译器不知道实际值范围而不是能够删除默认块(以及一些 cmp 指令等),但如果你不将此块添加到 switch 中,它会警告你。所以在这种情况下__builtin_unreachable 可能会有所帮助。

【讨论】:

  • “可能当您知道某些情况实际上是不可能的,但从逻辑上讲它可能会发生。” 使用 __builtin_unreachable(); 时要非常小心。默认情况下:案例标签。我花了几天时间试图找出 .rodata 区域的随机分支。在我的例子中,有 0 到 25 个案例标签和一个默认值:案例,用于从 0 到 128 的枚举。以及导致问题的开关值 71。 6.3.0 有范围检查代码,但 8.3.0 对其进行了优化。
【解决方案2】:

__builtin_unreachable 的目的是帮助编译器:

  • 删除死代码(程序员知道永远不会被执行)
  • 通过让编译器知道路径是“冷的”来线性化代码(调用noreturn函数可以达到类似的效果)

考虑以下几点:

void exit_if_true(bool x);

int foo1(bool x)
{
    if (x) {
        exit_if_true(true);
        //__builtin_unreachable(); // we do not enable it here
    } else {
        std::puts("reachable");
    }

    return 0;
}
int foo2(bool x)
{
    if (x) {
        exit_if_true(true);
        __builtin_unreachable();  // now compiler knows exit_if_true
                                  // will not return as we are passing true to it
    } else {
        std::puts("reachable");
    }

    return 0;
}

生成的代码:

foo1(bool):
        sub     rsp, 8
        test    dil, dil
        je      .L2              ; that jump is going to change
        mov     edi, 1
        call    exit_if_true(bool)
        xor     eax, eax         ; that tail is going to be removed
        add     rsp, 8
        ret
.L2:
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret
foo2(bool):
        sub     rsp, 8
        test    dil, dil
        jne     .L9              ; changed jump
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret
.L9:
        mov     edi, 1
        call    exit_if_true(bool)

注意区别:

  • xor eax, eaxret 已被删除,因为现在编译器知道这是一个死代码。
  • 编译器交换了分支的顺序:带有puts 调用的分支现在排在第一位,因此条件跳转可以更快(在预测和没有预测信息时,未采用的前向分支都更快)。李>

这里的假设是,以noreturn函数调用或__builtin_unreachable结尾的分支要么只执行一次,要么导致longjmp调用或异常抛出,这两种情况都很少见,不需要优先处理优化。

您正试图将它用于不同的目的 - 通过提供有关别名的编译器信息(您可以尝试为对齐做同样的事情)。不幸的是,GCC 不理解这样的地址检查。

如您所见,添加 __restrict__ 会有所帮助。所以__restrict__ 适用于别名,__builtin_unreachable 不适用。

看看下面这个使用__builtin_assume_aligned的例子:

void copy1(int *__restrict__ dst, const int *__restrict__ src)
{
    if (reinterpret_cast<uintptr_t>(dst) % 16 == 0) __builtin_unreachable();
    if (reinterpret_cast<uintptr_t>(src) % 16 == 0) __builtin_unreachable();
        
    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
    dst[3] = src[3];
}

void copy2(int *__restrict__ dst, const int *__restrict__ src)
{
    dst = static_cast<int *>(__builtin_assume_aligned(dst, 16));
    src = static_cast<const int *>(__builtin_assume_aligned(src, 16));

    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
    dst[3] = src[3];
}

生成的代码:

copy1(int*, int const*):
        movdqu  xmm0, XMMWORD PTR [rsi]
        movups  XMMWORD PTR [rdi], xmm0
        ret
copy2(int*, int const*):
        movdqa  xmm0, XMMWORD PTR [rsi]
        movaps  XMMWORD PTR [rdi], xmm0
        ret

您可以假设编译器可以理解 dst % 16 == 0 表示指针是 16 字节对齐的,但事实并非如此。因此使用了未对齐的存储和加载,而第二个版本生成更快的指令,需要对齐地址。

【讨论】:

  • 无法到达不仅仅是冷。 “冷”意味着很少执行,尤其是在启动或清理时。您可以在分支条件 (likely / unlikely macros) 上使用 __builtin_expect 或在函数上使用 __attribute__((cold)) 或 Profile-Guided Optimization (PGO) 告诉 GCC。除了那个术语挑剔之外,很好的答案。
  • 对于这种情况,正确的术语而不是“冷”是“死”(您在其他地方使用过,但不是同义词)。
  • 不,它是“冷”的,因为很少执行。以 unrechable 语句结尾的基本块被认为不太可能执行,就像使用 __builtin_expect 时一样。过去无法访问语句的代码是“死的”并被删除。该代码示例演示了如何根据代码生成器假设的分支概率来反转基本块顺序。
  • unreachable语句对分支概率的影响类似于调用noreturn函数或调用__attribute__((cold))函数。假设这样的分支在程序生命周期中不能执行多次,因为这是“冷”的(尽管longjmp 和异常违反了此规则)。在 C 中无法访问通常意味着代码路径导致 exitabort 调用。
猜你喜欢
  • 2012-04-26
  • 1970-01-01
  • 1970-01-01
  • 2011-11-10
  • 1970-01-01
  • 2010-10-07
  • 2012-12-05
  • 2011-04-05
  • 1970-01-01
相关资源
最近更新 更多