インラインアセンブリコメントを追加すると、GCCで生成されたコードにこのような根本的な変更が生じるのはなぜですか?


82

だから、私はこのコードを持っていました:

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

GCC4.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++;
    }
}

そしてGCCはこれを吐き出しました:

    .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コメントによって妨げられていますか?


28
GCC(およびほとんどのコンパイラ)がASMコンストラクトをブロックボックスのように扱うことを期待しています。したがって、彼らはそのような箱を通して何が起こるかについて推論することはできません。そして、それは多くの最適化を阻害します、特にそれらはループの境界を越えて実行されます。
Ira Baxter

10
asm空の出力リストとクローバーリストを使用して拡張フォームを試してください。
Kerrek SB 2012

4
@ R.MartinhoFernandes :(ドキュメントをasm("# im in ur loop" : : );参照)
Mike Seymour

16
-fverbose-asmフラグを追加することで、生成されたアセンブリを確認するときにもう少し助けが得られることに注意してください。フラグを追加すると、レジスタ間で物事がどのように移動しているかを識別するのに役立つ注釈が追加されます。
Matthew Slattery 2012

1
とても興味深い。ループの最適化を選択的に回避するために使用できますか?
シェプリン2012

回答:


62

最適化との相互作用は、ドキュメントの「C式オペランドを使用したアセンブラ命令」ページの約半分で説明されています。

GCCは、内部の実際のアセンブリを理解しようとはしませんasm。コンテンツについて知っているのは、(オプションで)出力および入力オペランドの指定とレジスタクローバーリストでそれを伝えることだけです。

特に、次の点に注意してください。

asm任意の出力オペランドのない命令は、揮発性と同一に扱われるasm命令。

そして

volatileキーワードは、命令が重要な副作用を持っていることを示しています[...]

したがって、asmGCCは副作用があると想定しているため、ループ内に存在することでベクトル化の最適化が妨げられています。


1
Basic Asmステートメントの副作用には、C ++コードが読み取り/書き込みを行うレジスタやメモリの変更を含めることはできません。しかし、はい、asmステートメントはC ++抽象マシンで実行されるたびに1回実行する必要があり、GCCはベクトル化せずに、1行あたり16回asmを発行することを選択しpaddbます。charアクセス​​はそうではないので、それは合法だと思いますvolatile。("memory"クローバーを使用した拡張asmステートメントとは異なり)
Peter Cordes

1
一般にGNUC Basic Asmステートメントを使用しない理由については、gcc.gnu.org / wiki / ConvertBasicAsmToExtendedを参照してください。このユースケース(単なるコメントマーカー)は、試してみるのが無理ではない数少ない例の1つです。
ピーターコーデス

23

gccがコードをベクトル化し、ループ本体を2つの部分に分割し、最初の部分は一度に16項目を処理し、2番目の部分は後で残りを処理することに注意してください。

Iraがコメントしたように、コンパイラはasmブロックを解析しないため、それが単なるコメントであることを認識しません。たとえそうだったとしても、それはあなたが何を意図したのかを知る方法がありません。最適化されたループはボディを2倍にします、それはあなたのasmをそれぞれに入れるべきですか?1000回も実行されないようにしませんか?わからないので、安全なルートをたどり、単純なシングルループにフォールバックします。


3

「gccはasm()ブロックの内容を理解していない」ということに同意しません。たとえば、gccはパラメーターの最適化、さらにasm()は生成されたCコードと混ざり合うようにブロックを再配置することさえも非常にうまく処理できます。これが、たとえばLinuxカーネルでインラインアセンブラを見る場合__volatile__、コンパイラが「コードを移動しない」ことを保証するために、ほとんどの場合、接頭辞が付いている理由です。gccに「rdtsc」を動かしてもらい、特定のことを行うのにかかった時間を測定しました。

文書化されているように、gccは特定のタイプのasm()ブロックを「特別な」ものとして扱うため、ブロックの両側のコードを最適化しません。

それは、gccがインラインアセンブラブロックによって混乱したり、アセンブラコードなどの結果に追随できないために特定の最適化をあきらめたりしないことを意味するわけではありません。さらに重要なのは、クローバータグがないために混乱することがよくあります-したがって、次のような指示がある場合cpuidこれによりEAX-EDXの値が変更されますが、EAXのみを使用するようにコードを記述した場合、コンパイラはEBX、ECX、およびEDXにデータを格納する可能性があり、これらのレジスタが上書きされるとコードが非常に奇妙に動作します...幸運なことに、すぐにクラッシュします。そうすれば、何が起こっているのかを簡単に理解できます。しかし、運が悪ければ、それは途中でクラッシュします...もう1つのトリッキーなものは、edxで2番目の結果を与えるdivide命令です。モジュロを気にしないのであれば、EDXが変更されたことを忘れがちです。


1
gccは、asmブロックの内容を実際には理解していません。拡張されたasmステートメントを介してそれを伝える必要があります。この追加情報がないと、gccはそのようなブロックを移動しません。gccは、あなたが述べた場合にも混乱しません-実際にはコードがそれらを壊してしまうのに、gccにそれらのレジスタを使用できると言って、プログラミングエラーを犯しただけです。
モニカを覚えておいてください2015年

返信が遅いですが、言う価値があると思います。volatile asmコードに「重要な副作用」がある可能性があることをGCCに通知し、より特別な注意を払って処理します。デッドコード最適化の一環として削除されるか、削除される可能性があります。Cコードとの相互作用は、そのような(まれな)ケースを想定し、厳密な順次評価を課す必要があります(たとえば、asm内に依存関係を作成することによって)。
edmz 2017

GNU C Basic asm(OPのようにオペランド制約なしasm(""))は、出力オペランドのない拡張asmと同様に、暗黙的に揮発性です。GCCはasmテンプレート文字列を理解せず、制約のみを理解します。そのため、制約を使用してコンパイラにasmを正確かつ完全に記述することが不可欠です。テンプレート文字列にオペランドを置き換えることはprintf、フォーマット文字列を使用すること以上の理解を必要としません。TL:DR:純粋なコメントを伴うこのようなユースケースを除いて、GNU C Basicasmを何にも使用しないでください。
ピーターコーデス

-2

この回答は現在変更されています。元々は、インラインBasic Asmをかなり強く指定されたツールと見なす考え方で書かれていましたが、GCCのようなものではありません。基本Asmが弱いので、答えを編集しました。

各アセンブリコメントはブレークポイントとして機能します。

編集:しかし、あなたが基本的なAsmを使用するので、壊れたもの。明示的なクローバーリストのないインラインasmasm関数本体内のステートメント)は、GCCで弱く指定された機能であり、その動作を定義するのは困難です。特に何かに付けられているようには見えないので(私はその保証を完全には理解していません)、関数を実行する場合はある時点でアセンブリコードを実行する必要がありますが、それ以外の場合はいつ実行するかは明確ではありません些細な最適化レベル。隣接する命令で並べ替えることができるブレークポイントは、あまり有用な「ブレークポイント」ではありません。編集終了

コメントごとに中断し、すべての変数の状態を出力するインタープリターでプログラムを実行できます(デバッグ情報を使用)。これらのポイントは、環境(レジスタとメモリの状態)を監視するために存在する必要があります。

コメントがない場合、観測点は存在せず、ループは、環境を取得して変更された環境を生成する単一の数学関数としてコンパイルされます。

意味のない質問の答えを知りたい:各命令(またはブロック、あるいは命令の範囲)がどのようにコンパイルされるかを知りたいが、単一の分離された命令(またはブロック)はコンパイルされない。すべてのものが全体としてコンパイルされます。

より良い質問は次のとおりです。

こんにちはGCC。このasm出力がソースコードを実装していると思うのはなぜですか?あらゆる前提で、段階的に説明してください。

ただし、GCC内部表現で記述されたasm出力よりも長い証明を読みたくない場合があります。


1
これらのポイントは、環境(レジスタとメモリの状態)を監視するために存在する必要があります。-これは、最適化されていないコードに当てはまる可能性があります。最適化を有効にすると、関数全体がバイナリから消える可能性があります。ここでは、最適化されたコードについて話します。
Bartek Banachewicz 2015年

1
最適化を有効にしてコンパイルした結果として生成されるアセンブリについて話します。したがって、あなたは何かが存在しなければならないと述べるのは間違っています。
Bartek Banachewicz 2015年

1
ええ、IDKはなぜ誰もがそうするのか、そして誰もそうすべきではないことに同意します。私の最後のコメントのリンクが説明しているように、誰もそうすべきではなく"memory"、確かに存在する既存のバグのあるコードのバンドエイドとしてそれを強化することについて(例えば暗黙のクローバーで)議論がありました。asm("cli")そのような命令がコンパイラーで生成されたコードが触れないアーキテクチャー状態の一部にのみ影響する場合でも、wrtで注文する必要があります。コンパイラによって生成されたロード/ストア(たとえば、クリティカルセクション周辺の割り込みを無効にしている場合)。
ピーターコーデス

1
レッドゾーンを壊すのは安全ではないため、add rsp, -128最初に実行しない限り、asmステートメント内のレジスタの非効率的な手動保存/復元(プッシュ/ポップを使用)でも安全ではありません。しかし、それを行うことは明らかに頭がおかしいです。
ピーターコーデス

1
現在、GCCはBasic Asmをまったく同等に扱いますasm("" :::)(出力がないため暗黙的に揮発性ですが、入力または出力の依存関係によってコードの残りの部分に関連付けられていません。また、"memory"クローバーもありません)。そしてもちろん%operand、テンプレート文字列の置換%は行わないため、リテラルをとしてエスケープする必要はありません%%。そうです、同意しました。__attribute__((naked))関数とグローバルスコープの外でBasicAsmを非推奨にするのは良い考えです。
ピーターコーデス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.