私はこの単純な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コードを確認しようとしましたが、結果は明確ではありませんでした。その前のもの。
誰かが簡単に説明できますか:
コンパイラはそれよりもさらに賢いです。 :)
実際、ループの結果を使用していないことがわかります。だから、ループ全体を完全に取り出しました!
これは デッドコード除去 と呼ばれます。
より良いテストは、結果を印刷することです。
_#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
_
コンパイラには、コードをより効率的または「効率的」にするためのツールがいくつかあります。
計算の結果が使用されない場合は、計算を実行するコードを省略できます(計算がvolatile
値に基づいて実行された場合、それらの値を読み取る必要がありますが、読み取った結果は無視できます)。それを供給した計算の結果が使用されなかった場合、それらを実行するコードも省略できます。このような省略により、条件分岐の両方のパスのコードが同一になる場合、条件は未使用と見なされ、省略される可能性があります。これは、範囲外のメモリアクセスを行わない、またはAnnexLが「重要な未定義の動作」と呼ぶものを呼び出さないプログラムの動作(実行時間以外)には影響しません。
コンパイラは、値を計算するマシンコードが特定の範囲の結果しか生成できないと判断した場合、それに基づいて結果を予測できる条件付きテストを省略できます。上記のように、コードが「Critical Undefined Behaviors」を呼び出さない限り、これは実行時間以外の動作には影響しません。
コンパイラが、特定の入力が記述されたコードで任意の形式の未定義の動作を呼び出すと判断した場合、標準では、実行プラットフォームの自然な動作であっても、そのような入力を受け取ったときにのみ関連するコードをコンパイラが省略できます。そのような入力が与えられれば、それは無害であり、コンパイラの書き直しはそれを危険なものにするでしょう。
優れたコンパイラは#1と#2を実行します。しかし、どういうわけか、#3はファッショナブルになっています。