web-dev-qa-db-ja.com

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

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

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

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

81

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

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

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

出力オペランドのないasm命令は、揮発性asm命令と同じように扱われます。

そして

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

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

58

Gccはコードをベクトル化し、ループ本体を2つの部分に分割します。最初は一度に16項目を処理し、2番目は残りを後で処理します。

Iraがコメントしたように、コンパイラーはasmブロックを解析しないので、それが単なるコメントであることを知りません。それがあったとしても、それはあなたが何を意図していたかを知る方法がありません。最適化されたループは本体が2倍になります。それぞれにasmを配置する必要がありますか? 1000回実行しないでよろしいですか?わからないので、安全な経路をたどって、単純な単一ループにフォールバックします。

21
Jester

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

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

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

4
Mats Petersson