それでは,最大限の最適化を行った場合はどのくらい計算時間が短縮されるのだろうか. Visual C/C++のclコマンドに最大限の最適化を行う/Oxオプションを付けてコンパイルし, 作成されたバイナリを実行したところ,Pentium!!! 600MHzで計算時間は2.08秒であった. 最適化を一切行わない場合が6.70秒であるから,実に3分の1以下の計算時間である.
最適化を行わない場合と行った場合でそれぞれどのような実行バイナリが作られるかを検証するために,
/Faオプションにより作成されるアセンブラコードを比較してみた.
以下はベンチマークプログラムのmain関数である.
アセンブラコードとの比較のため,左側に行番号を示しておく.
一切の最適化を行わない場合,次のようなアセンブラコードが生成された.
10 int main(){
11
12 double a, b, c, d, roots[ 3 ];
13 int i, n;
14 DWORD started, terminated;
15
16 started = GetCurrentTime();
17 for ( i = 1; i <= 1000000; i++ ){
18 a = rand(); b = rand(); c = rand(); d = rand();
19 if ( a != 0 ) n = cubic_equation( a, b, c, d, roots );
20 }
21 terminated = GetCurrentTime();
22 printf( " total %10.3f seconds\n", ( terminated - started ) * 1e-3 );
23 return 0;
24 }
最適化を行っていないのだから当たり前といえば当たり前だが,
もとのCのコードが忠実に機械語に変換されている.
例えば,カウンタであるiの値をレジスタに読み込み,レジスタに1を加算してiに書き戻し,
iと1,000,000を比較するという処理が1,000,000繰り返されている.
また,cubic_equationに実引数を渡す際には,後の実引数から順序良く4バイトずつスタックにプッシュされている.
rootsは配列でポインタだから1回のプッシュで済むが,
a,b,c,dはdoubleで8バイトだから,それぞれ2回に分けてプッシュされている.
_main PROC NEAR
; File main.c
; Line 10
push ebp
mov ebp, esp
sub esp, 96 ; 00000060H
; Line 16
call DWORD PTR __imp__GetTickCount@0
mov DWORD PTR _started$[ebp], eax
; Line 18
mov DWORD PTR _i$[ebp], 1 ; iに1が代入される
jmp SHORT $L21941 ; $L21941にジャンプ
$L21942:
mov eax, DWORD PTR _i$[ebp] ; iの値がeaxレジスタに読み込まれる
add eax, 1 ; eaxレジスタに1を加える
mov DWORD PTR _i$[ebp], eax ; eaxレジスタの値をiに書き戻す
$L21941:
cmp DWORD PTR _i$[ebp], 1000000 ; 000f4240H ; iと1000000を比較
jg SHORT $L21943 ; i > 1000000とき$L21943にジャンプ
; Line 19
call _rand
mov DWORD PTR -76+[ebp], eax
fild DWORD PTR -76+[ebp]
fstp QWORD PTR _a$[ebp]
call _rand
mov DWORD PTR -80+[ebp], eax
fild DWORD PTR -80+[ebp]
fstp QWORD PTR _b$[ebp]
call _rand
mov DWORD PTR -84+[ebp], eax
fild DWORD PTR -84+[ebp]
fstp QWORD PTR _c$[ebp]
call _rand
mov DWORD PTR -88+[ebp], eax
fild DWORD PTR -88+[ebp]
fstp QWORD PTR _d$[ebp]
; Line 20
fld QWORD PTR _a$[ebp]
fcomp QWORD PTR $T21980
fnstsw ax
test ah, 64 ; 00000040H
jne SHORT $L21944
lea ecx, DWORD PTR _roots$[ebp] ; roots配列のアドレスをecxレジスタにコピー
push ecx ; ecxレジスタの値をスタックにプッシュ
mov edx, DWORD PTR _d$[ebp+4] ; dの上位バイトをedxレジスタにコピー
push edx ; edxレジスタの値をスタックにプッシュ
mov eax, DWORD PTR _d$[ebp] ; dの下位バイトをeaxレジスタにコピー
push eax ; eaxレジスタの値をスタックにプッシュ
mov ecx, DWORD PTR _c$[ebp+4] ; 以下同じ
push ecx
mov edx, DWORD PTR _c$[ebp]
push edx
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call _cubic_equation
add esp, 36 ; 00000024H
mov DWORD PTR _n$[ebp], eax
$L21944:
; Line 21
jmp $L21942
$L21943:
; Line 23
call DWORD PTR __imp__GetTickCount@0
mov DWORD PTR _terminated$[ebp], eax
; Line 24
mov ecx, DWORD PTR _terminated$[ebp]
sub ecx, DWORD PTR _started$[ebp]
mov DWORD PTR -96+[ebp], ecx
mov DWORD PTR -96+[ebp+4], 0
fild QWORD PTR -96+[ebp]
fmul QWORD PTR $T21981
sub esp, 8
fstp QWORD PTR [esp]
push OFFSET FLAT:$SG21945
call _printf
add esp, 12 ; 0000000cH
; Line 25
xor eax, eax
; Line 26
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
最大限の最適化を行った場合のアセンブラコードは以下のようになった.
こちらのコードではカウンタとなる変数を1ずつ増加させて行くのではなく,カウンタとなるesiレジスタに
最初1,000,000を記憶させておいて,ループの度に1ずつesiレジスタの値を減少させるようになっている.
コードの通りに変数をカウンタとして使うと,加算および比較の際にカウンタの値をレジスタに
読み込まなければならないが,最初からレジスタをカウンタとして使えば,
このようなオーバーヘッドをなくすことができる.
; File main.c
; Line 10
push ebp
mov ebp, esp
and esp, -8 ; fffffff8H
sub esp, 60 ; 0000003cH
push ebx
push esi
push edi
; Line 16
mov edi, DWORD PTR __imp__GetTickCount@0
call edi
mov ebx, eax
mov esi, 1000000 ; 000f4240H ; esiレジスタに1000000を代入
$L21966:
; Line 19
call _rand
mov DWORD PTR -56+[esp+72], eax
fild DWORD PTR -56+[esp+72]
fstp QWORD PTR _a$[esp+72]
call _rand
mov DWORD PTR -56+[esp+72], eax
fild DWORD PTR -56+[esp+72]
fstp QWORD PTR _b$[esp+72]
call _rand
mov DWORD PTR -56+[esp+72], eax
fild DWORD PTR -56+[esp+72]
fstp QWORD PTR _c$[esp+72]
call _rand
mov DWORD PTR -56+[esp+72], eax
fild DWORD PTR -56+[esp+72]
fstp QWORD PTR _d$[esp+72]
; Line 20
fld QWORD PTR _a$[esp+72]
fcomp QWORD PTR $T22007
fnstsw ax
test ah, 64 ; 00000040H
jne SHORT $L21967
mov ecx, DWORD PTR _d$[esp+76]
mov edx, DWORD PTR _d$[esp+72]
lea eax, DWORD PTR _roots$[esp+72]
push eax
mov eax, DWORD PTR _c$[esp+80]
push ecx
mov ecx, DWORD PTR _c$[esp+80]
push edx
mov edx, DWORD PTR _b$[esp+88]
push eax
mov eax, DWORD PTR _b$[esp+88]
push ecx
mov ecx, DWORD PTR _a$[esp+96]
push edx
mov edx, DWORD PTR _a$[esp+96]
push eax
push ecx
push edx
call _cubic_equation
add esp, 36 ; 00000024H
$L21967:
; Line 18
dec esi ; esiレジスタから1を引く
jne $L21966 ; ZFフラグが0でなければ$L21966にジャンプ
; Line 23
call edi
; Line 24
sub eax, ebx
mov DWORD PTR -32+[esp+76], 0
mov DWORD PTR -32+[esp+72], eax
sub esp, 8
fild QWORD PTR -32+[esp+80]
fmul QWORD PTR $T22008
fstp QWORD PTR [esp]
push OFFSET FLAT:$SG21970
call _printf
add esp, 12 ; 0000000cH
; Line 25
xor eax, eax
; Line 26
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
また,dec命令は減算した結果が0になったとき自動的にZFフラグを0にするので, jne命令と組み合わせればcmp命令によるカウンタの比較を行う必要がない. jne命令はZFフラグの値をチェックし,それが0でないときに限りジャンプするようになっているためである.
さらにcubic_equationに実引数を渡す際も変則的である. どういう意図でこのような順序になっているのかはわからないが,レジスタの値とスタックにプッシュされる 値を追っかけてみると,ちゃんとroots,d,c,b,aの順にプッシュされている.
SEO | [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送 | ||