web-dev-qa-db-ja.com

GCCは、ループ内でインクリメントされた未使用の変数をどのように最適化しますか?

私はこの単純なCプログラムを書きました:

_int main() {
    int i;
    int count = 0;
    for(i = 0; i < 2000000000; i++){
        count = count + 1;
    }
}
_

Gccコンパイラがこのループをどのように最適化するかを見たかった(明らかにadd12000000000回は "add20000000001回 ")。そう:

gcc test.cそして_a.out_のtimeは次のようになります。

_real 0m7.717s  
user 0m7.710s  
sys 0m0.000s  
_

$ gcc -O2 test.cそして_time on_ a.out`は次のようになります。

_real 0m0.003s  
user 0m0.000s  
sys 0m0.000s  
_

次に、両方を_gcc -S_で分解しました。最初のものは非常に明確に見えます:

_    .file "test.c"  
    .text  
.globl main
    .type   main, @function  
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    $0, -8(%rbp)
    movl    $0, -4(%rbp)
    jmp .L2
.L3:
    addl    $1, -8(%rbp)
    addl    $1, -4(%rbp)
.L2:
    cmpl    $1999999999, -4(%rbp)
    jle .L3
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section    .note.GNU-stack,"",@progbits
_

L3は追加し、L2は-4(%rbp)を_1999999999_と比較し、_i < 2000000000_の場合はL3にループします。

最適化されたもの:

_    .file "test.c"  
    .text
    .p2align 4,,15
.globl main
    .type main, @function
main:
.LFB0:
    .cfi_startproc
    rep
    ret
    .cfi_endproc
.LFE0:
    .size main, .-main
    .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section .note.GNU-stack,"",@progbits
_

何が起こっているのか全く理解できません!アセンブリの知識はほとんどありませんが、次のようなものを期待していました

_addl $2000000000, -8(%rbp)
_

gcc -c -g -Wa、-a、-ad -O2 test.cを使用して、変換されたアセンブリと一緒にCコードを確認しようとしましたが、結果は明確ではありませんでした。その前のもの。

誰かが簡単に説明できますか:

  1. gcc -S -O2出力。
  2. ループが期待どおりに最適化されている場合(多くの合計ではなく1つの合計)?
64
Haile

コンパイラはそれよりもさらに賢いです。 :)

実際、ループの結果を使用していないことがわかります。だから、ループ全体を完全に取り出しました!

これは デッドコード除去 と呼ばれます。

より良いテストは、結果を印刷することです。

_#include <stdio.h>
int main(void) {
    int i; int count = 0;
    for(i = 0; i < 2000000000; i++){
        count = count + 1;
    }

    //  Print result to prevent Dead Code Elimination
    printf("%d\n", count);
}
_

編集:必要な_#include <stdio.h>_を追加しました; MSVCアセンブリのリストは、_#include_のないバージョンに対応していますが、同じである必要があります。


Windowsを起動しているので、現時点ではGCCが目の前にありません。ただし、MSVCでprintf()を使用したバージョンの逆アセンブリは次のとおりです。

編集:アセンブリ出力が間違っていました。これが正しいものです。

_; 57   : int main(){

$LN8:
    sub rsp, 40                 ; 00000028H

; 58   : 
; 59   : 
; 60   :     int i; int count = 0;
; 61   :     for(i = 0; i < 2000000000; i++){
; 62   :         count = count + 1;
; 63   :     }
; 64   : 
; 65   :     //  Print result to prevent Dead Code Elimination
; 66   :     printf("%d\n",count);

    lea rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6?$AA@
    mov edx, 2000000000             ; 77359400H
    call    QWORD PTR __imp_printf

; 67   : 
; 68   : 
; 69   : 
; 70   :
; 71   :     return 0;

    xor eax, eax

; 72   : }

    add rsp, 40                 ; 00000028H
    ret 0
_

そうです、VisualStudioはこの最適化を行います。 GCCもおそらくそうだと思います。

はい、GCCは同様の最適化を実行します。 _gcc -S -O2 test.c_(gcc 4.5.2、Ubuntu 11.10、x86)を使用した同じプログラムのアセンブリリストは次のとおりです。

_        .file   "test.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "%d\n"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $16, %esp
        movl    $2000000000, 8(%esp)
        movl    $.LC0, 4(%esp)
        movl    $1, (%esp)
        call    __printf_chk
        leave
        ret
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
        .section        .note.GNU-stack,"",@progbits
_
73
Mysticial

コンパイラには、コードをより効率的または「効率的」にするためのツールがいくつかあります。

  1. 計算の結果が使用されない場合は、計算を実行するコードを省略できます(計算がvolatile値に基づいて実行された場合、それらの値を読み取る必要がありますが、読み取った結果は無視できます)。それを供給した計算の結果が使用されなかった場合、それらを実行するコードも省略できます。このような省略により、条件分岐の両方のパスのコードが同一になる場合、条件は未使用と見なされ、省略される可能性があります。これは、範囲外のメモリアクセスを行わない、またはAnnexLが「重要な未定義の動作」と呼ぶものを呼び出さないプログラムの動作(実行時間以外)には影響しません。

  2. コンパイラは、値を計算するマシンコードが特定の範囲の結果しか生成できないと判断した場合、それに基づいて結果を予測できる条件付きテストを省略できます。上記のように、コードが「Critical Undefined Behaviors」を呼び出さない限り、これは実行時間以外の動作には影響しません。

  3. コンパイラが、特定の入力が記述されたコードで任意の形式の未定義の動作を呼び出すと判断した場合、標準では、実行プラットフォームの自然な動作であっても、そのような入力を受け取ったときにのみ関連するコードをコンパイラが省略できます。そのような入力が与えられれば、それは無害であり、コンパイラの書き直しはそれを危険なものにするでしょう。

優れたコンパイラは#1と#2を実行します。しかし、どういうわけか、#3はファッショナブルになっています。

1
supercat