簡體   English   中英

__builtin_unreachable 促進了哪些優化?

[英]What optimizations does __builtin_unreachable facilitate?

從 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;
}

編譯為(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讓它編譯成同樣的東西嗎?

__builtin_unreachable的目的是幫助編譯器刪除死代碼(程序員知道永遠不會被執行)並通過讓編譯器知道路徑是“冷”來線性化代碼。 考慮以下:

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字節對齊的,但事實並非如此。 因此使用未對齊的存儲和加載,而第二個版本生成更快的指令,需要對齊地址。

我認為你是試圖微量優化你的代碼錯誤的方向錯誤。

__builtin_unreachable以及__builtin_expect執行預期的操作 - 在您的情況下jnz使用的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運算符和未達到的指令會影響編譯器優化器,因此無法進行優化。

在交換兩個整數的情況下,第三個臨時交換變量可以通過編譯器自己刪除,即它會像

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? 可能當你知道某些情況幾乎不可能時,但邏輯上可能會發生。 即你有一些功能

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指令等),但如果你不將這個塊添加到交換機中,它會警告你。 所以在這種情況下__builtin_unreachable可能有所幫助。

可能當您知道某些情況實際上是不可能的,但從邏輯上講它可能會發生。

使用 __builtin_unreachable() 時要非常小心; 默認情況下:案例標簽。 我花了幾天時間試圖找出 .rodata 區域的隨機分支。 在我的例子中,有 0 到 25 個案例標簽和一個默認值:案例,用於從 0 到 128 的枚舉。以及導致問題的開關值 71。 6.3.0 有范圍檢查代碼,但 8.3.0 對其進行了優化。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM