簡體   English   中英

為什么添加內聯匯編注釋會導致 GCC 生成的代碼發生如此大的變化?

[英]Why does adding inline assembly comments cause such radical change in GCC's generated code?

所以,我有這個代碼:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        *sum++ = *a++ + *b++;
    }
}

我想看看 GCC 4.7.2 會生成的代碼。 所以我運行g++ -march=native -O3 -masm=intel -S a.c++ -std=c++11並得到以下輸出:

        .file   "a.c++"
        .intel_syntax noprefix
        .text
        .p2align 4,,15
        .globl  _Z2f1PcS_S_
        .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L5
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L5
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L3:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L3
        mov     ax, 8
        mov     r9d, 992
.L2:
        sub     eax, 1
        lea     rcx, [rdx+r9]
        add     rdi, r9
        lea     r8, [rax+1]
        add     rsi, r9
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L4:
        movzx   edx, BYTE PTR [rcx+rax]
        add     dl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], dl
        add     rax, 1
        cmp     rax, r8
        jne     .L4
        rep
        ret
.L5:
        mov     eax, 1000
        xor     r9d, r9d
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
        .p2align 4,,15
        .globl  _Z2f2PcS_S_
        .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
        .cfi_startproc
        lea     rcx, [rdx+16]
        lea     rax, [rdi+16]
        cmp     rdi, rcx
        setae   r8b
        cmp     rdx, rax
        setae   cl
        or      cl, r8b
        je      .L19
        lea     rcx, [rsi+16]
        cmp     rdi, rcx
        setae   cl
        cmp     rsi, rax
        setae   al
        or      cl, al
        je      .L19
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L17:
        movdqu  xmm0, XMMWORD PTR [rdx+rax]
        movdqu  xmm1, XMMWORD PTR [rsi+rax]
        paddb   xmm0, xmm1
        movdqu  XMMWORD PTR [rdi+rax], xmm0
        add     rax, 16
        cmp     rax, 992
        jne     .L17
        add     rdi, 992
        add     rsi, 992
        add     rdx, 992
        mov     r8d, 8
.L16:
        xor     eax, eax
        .p2align 4,,10
        .p2align 3
.L18:
        movzx   ecx, BYTE PTR [rdx+rax]
        add     cl, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, r8
        jne     .L18
        rep
        ret
.L19:
        mov     r8d, 1000
        jmp     .L16
        .cfi_endproc
.LFE1:
        .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
        .ident  "GCC: (GNU) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

我不擅長閱讀匯編,所以我決定添加一些標記來了解循環體的去向:

constexpr unsigned N = 1000;
void f1(char* sum, char* a, char* b) {
    for(int i = 0; i < N; ++i) {
        asm("# im in ur loop");
        sum[i] = a[i] + b[i];
    }
}

void f2(char* sum, char* a, char* b) {
    char* end = sum + N;
    while(sum != end) {
        asm("# im in ur loop");
        *sum++ = *a++ + *b++;
    }
}

海灣合作委員會吐出了這個:

    .file   "a.c++"
    .intel_syntax noprefix
    .text
    .p2align 4,,15
    .globl  _Z2f1PcS_S_
    .type   _Z2f1PcS_S_, @function
_Z2f1PcS_S_:
.LFB0:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L2:
#APP
# 4 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L2
    rep
    ret
    .cfi_endproc
.LFE0:
    .size   _Z2f1PcS_S_, .-_Z2f1PcS_S_
    .p2align 4,,15
    .globl  _Z2f2PcS_S_
    .type   _Z2f2PcS_S_, @function
_Z2f2PcS_S_:
.LFB1:
    .cfi_startproc
    xor eax, eax
    .p2align 4,,10
    .p2align 3
.L6:
#APP
# 12 "a.c++" 1
    # im in ur loop
# 0 "" 2
#NO_APP
    movzx   ecx, BYTE PTR [rdx+rax]
    add cl, BYTE PTR [rsi+rax]
    mov BYTE PTR [rdi+rax], cl
    add rax, 1
    cmp rax, 1000
    jne .L6
    rep
    ret
    .cfi_endproc
.LFE1:
    .size   _Z2f2PcS_S_, .-_Z2f2PcS_S_
    .ident  "GCC: (GNU) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

這是相當短的,並且有一些顯着差異,例如缺少 SIMD 指令。 我期待相同的輸出,中間有一些評論。 我在這里做了一些錯誤的假設嗎? GCC 的優化器是否受到 asm 注釋的阻礙?

與優化的交互在文檔中的 “帶有 C 表達式操作數的匯編器指令”頁面的一半部分進行了解釋。

GCC 不會試圖理解asm任何實際程序集; 它唯一知道的內容是您(可選)在輸出和輸入操作數規范以及寄存器破壞列表中告訴它的內容。

特別要注意:

沒有任何輸出操作數的asm指令將被視為與易失性asm指令相同。

volatile關鍵字表示該指令具有重要的副作用 [...]

所以循環中asm的存在抑制了矢量化優化,因為 GCC 假設它有副作用。

請注意,gcc 對代碼進行了矢量化,將循環體分為兩部分,第一部分一次處理 16 項,第二部分稍后處理其余部分。

正如 Ira 評論的那樣,編譯器不解析 asm 塊,因此它不知道這只是一個注釋。 即使這樣做了,它也無法知道您的意圖。 優化的循環使主體加倍,是否應該將您的 asm 放入每個循環中? 你希望它不被執行 1000 次嗎? 它不知道,所以它走安全路線並退回到簡單的單循環。

我不同意“gcc 不了解asm()塊中的內容”。 例如,gcc 可以很好地處理優化參數,甚至可以重新排列asm()塊,使其與生成的 C 代碼混合。 這就是為什么,如果您查看例如 Linux 內核中的內聯匯編器,它幾乎總是以__volatile__為前綴,以確保編譯器“不會移動代碼”。 我讓 gcc 移動了我的“rdtsc”,這讓我測量了做某件事所需的時間。

如文檔所述,gcc 將某些類型的asm()塊視為“特殊”塊,因此不會優化塊兩側的代碼。

這並不是說 gcc 有時不會被內聯匯編程序塊弄糊塗,或者只是決定放棄某些特定的優化,因為它無法遵循匯編代碼等的結果。更重要的是,它經常會因為缺少 clobber 標簽而感到困惑——所以如果你有一些像cpuid這樣的指令改變了 EAX-EDX 的值,但是你編寫了代碼以便它只使用 EAX,編譯器可能會在 EBX、ECX 和 EDX 中存儲東西,然后當這些寄存器被覆蓋時,您的代碼表現得非常奇怪……如果幸運的話,它會立即崩潰 - 那么很容易弄清楚發生了什么。 但是如果你不走運,它就會崩潰……另一個棘手的問題是在 edx 中給出第二個結果的除法指令。 如果你不關心模數,很容易忘記 EDX 被改變了。

現在修改了這個答案:它最初是在考慮將內聯 Basic Asm 作為一個非常明確的工具時的思維方式編寫的,但它與 GCC 中的完全不同。 基本 Asm 很弱,因此對答案進行了編輯。

每個程序集注釋都充當斷點。

編輯:但是一個壞掉的,因為你使用基本的 Asm。 沒有顯式 clobber 列表的內聯asm (函數體內的asm語句)是 GCC 中弱指定的特性,其行為很難定義。 似乎沒有(我不完全掌握它的擔保)連接到某些特定的東西,所以當組合代碼必須在一些點上運行,如果運行的功能,目前尚不清楚,當它運行的任何非微不足道的優化級別 可以用相鄰指令重新排序的斷點不是一個非常有用的“斷點”。 結束編輯

您可以在解釋器中運行您的程序,該解釋器會在每個注釋處中斷並打印出每個變量的狀態(使用調試信息)。 這些點必須存在,以便您觀察環境(寄存器和內存的狀態)。

如果沒有注釋,則不存在觀察點,並且循環被編譯為單個數學函數,采用環境並生成修改后的環境。

你想知道一個毫無意義的問題的答案:你想知道每條指令(或者可能是塊,或者可能是指令范圍)是如何編譯的,但沒有單獨的指令(或塊)被編譯; 整個東西是作為一個整體編譯的。

一個更好的問題是:

你好海合會。 為什么你相信這個 asm 輸出正在實現源代碼? 請一步一步解釋每一個假設。

但是,您不希望閱讀比 asm 輸出更長的證明,根據 GCC 內部表示編寫。

暫無
暫無

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

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