アセンブラコードの比較


ここに1,000,000個の3次方程式を解くベンチマークプログラムのことを書いたが, 各マシンでコンパイルする際には,一切の最適化を抑制してコンパイルした. コンパイラの性能差をできる限り排除して,マシン性能が計算時間に正確に反映されるようにするためである.

それでは,最大限の最適化を行った場合はどのくらい計算時間が短縮されるのだろうか. 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   }
一切の最適化を行わない場合,次のようなアセンブラコードが生成された.
_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
最適化を行っていないのだから当たり前といえば当たり前だが, もとのCのコードが忠実に機械語に変換されている. 例えば,カウンタであるiの値をレジスタに読み込み,レジスタに1を加算してiに書き戻し, iと1,000,000を比較するという処理が1,000,000繰り返されている. また,cubic_equationに実引数を渡す際には,後の実引数から順序良く4バイトずつスタックにプッシュされている. rootsは配列でポインタだから1回のプッシュで済むが, a,b,c,dはdoubleで8バイトだから,それぞれ2回に分けてプッシュされている.

最大限の最適化を行った場合のアセンブラコードは以下のようになった.

; 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
こちらのコードではカウンタとなる変数を1ずつ増加させて行くのではなく,カウンタとなるesiレジスタに 最初1,000,000を記憶させておいて,ループの度に1ずつesiレジスタの値を減少させるようになっている. コードの通りに変数をカウンタとして使うと,加算および比較の際にカウンタの値をレジスタに 読み込まなければならないが,最初からレジスタをカウンタとして使えば, このようなオーバーヘッドをなくすことができる.

また,dec命令は減算した結果が0になったとき自動的にZFフラグを0にするので, jne命令と組み合わせればcmp命令によるカウンタの比較を行う必要がない. jne命令はZFフラグの値をチェックし,それが0でないときに限りジャンプするようになっているためである.

さらにcubic_equationに実引数を渡す際も変則的である. どういう意図でこのような順序になっているのかはわからないが,レジスタの値とスタックにプッシュされる 値を追っかけてみると,ちゃんとroots,d,c,b,aの順にプッシュされている.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送