低レベルプログラミング(No.06:条件分岐)

使用ツール:Visual Studio Community 2019
使用言語 :C言語

条件分岐

条件分岐は様々な所で利用されます。

  • if~else if~else
  • switch~case
  • forループ
  • while、do~whileループ

今回はif分をメインに見て行きます。

サンプルコード

今回使用するC言語のサンプルコードは下記です。(シンプルですね)

#include <stdio.h>

void main()
{
	int a = -100;
	int b = 10;
	
	if(a > b)
	{
		printf("aはbより大きい\n");
	}
	else if (a == b)
	{
		printf("aとbは等しい\n");
	}
	else
	{
		printf("aはbより小さい\n");
	}	
}

プログラム中に日本語(全角文字)が含まれていると、下記の様な警告メッセージが表示される場合がありますが、その場合は、ソースコードの文字コードをSJIS(Shift-JIS)で保存してください。

※最近は、UTF-8で統一されつつありますが、文字コードの違いによる不具合も昔は良くありました…。

プログラム自体は「変数aと変数bを比較して大きいか小さいかを出力する」ただそれだけのプログラムです。

アセンブリコード

「cl.exe /FA sample_006_1.c」を実行することで、アセンブリコード(sample_006_1.asm)が生成されます。

アセンブリコードの、main関数部分(5~19行目)のみ抜き出したのが、下記コードになります。

; Line 5
	mov	DWORD PTR _a$[ebp], -100		; ffffff9cH
; Line 6
	mov	DWORD PTR _b$[ebp], 10			; 0000000aH
; Line 8
	mov	eax, DWORD PTR _a$[ebp]
	cmp	eax, DWORD PTR _b$[ebp]
	jle	SHORT $LN2@main
; Line 10
	push	OFFSET $SG9252
	call	_printf
	add	esp, 4
; Line 11
	jmp	SHORT $LN1@main
$LN2@main:
; Line 12
	mov	ecx, DWORD PTR _a$[ebp]
	cmp	ecx, DWORD PTR _b$[ebp]
	jne	SHORT $LN4@main
; Line 14
	push	OFFSET $SG9255
	call	_printf
	add	esp, 4
; Line 15
	jmp	SHORT $LN1@main
$LN4@main:
; Line 18
	push	OFFSET $SG9256
	call	_printf
	add	esp, 4
$LN1@main:

条件分岐を含むコードだと、下記の様なラベルがアセンブリコードに現れてきます。

  • $LN2@main:
  • $LN4@main:
  • $LN1@main:

これらは、プログラム領域上のアドレスを指し示しています。

※コード中のラベル部分は最終的なプログラム(バイナリデータ)になると、実際のアドレスの値が入ります。

最初のifブロックのCソースコードとアセンブリコードを並べてみると下記の様になります。

	if(a > b)
	{
		printf("aはbより大きい\n");
	}
 Line 8
	mov	eax, DWORD PTR _a$[ebp]
	cmp	eax, DWORD PTR _b$[ebp]
	jle	SHORT $LN2@main
; Line 10
	push	OFFSET $SG9252
	call	_printf
	add	esp, 4
; Line 11
	jmp	SHORT $LN1@main
$LN2@main:
$LN1@main:

45行目は「mov」命令で変数aの値をレジスタAXに格納しています。

46行目の「cmp」比較命令は、レジスタAX(変数aの値)から変数bの値を引いて、その結果に応じた各種フラグ(EFLAGS)を設定します。

※「各種フラグ(EFLAGS)」とは、等しいか(ゼロか)、符号が立ったか、オーバーフローしたかなど、計算結果に応じてセットされるフラグセットですが、詳細は割愛します。

47行目の「jle」(”Jump Less Equal”)命令は、直前のcmp命令の結果が0以下なら(a – bの結果が0以下なら)指定のラベル($LN2@main)までジャンプする(PC:プログラムカウンタをすすめる)という意味になります。

ここで、処理が分岐する事になります。

48~51行目は、printf関数の実行に該当する部分です。

53行目は、「jmp」命令で「$LN1@main」の位置までジャンプし、else if~else部分をスキップしています。


続いて、else ifブロックのCソースコードとアセンブリコードを並べてみると下記の様になります。

	else if (a == b)
	{
		printf("aとbは等しい\n");
	}
; Line 12
	mov	ecx, DWORD PTR _a$[ebp]
	cmp	ecx, DWORD PTR _b$[ebp]
	jne	SHORT $LN4@main
; Line 14
	push	OFFSET $SG9255
	call	_printf
	add	esp, 4
; Line 15
	jmp	SHORT $LN1@main
$LN4@main:
$LN1@main:

56行目は「mov」命令で変数aの値をレジスタCXに格納しています。

57行目の「cmp」比較命令は、レジスタCX(変数aの値)から変数bの値を引いて、その結果に応じた各種フラグ(EFLAGS)を設定します。

58行目の「jne」(”Jump Not Equal”)命令は、直前のcmp命令の結果が0でないなら(a – bの結果が0出ないなら)、ラベル($LN4@main)までジャンプするという意味になります。

※分岐処理は、Cソースコードの条件の反対の判定をして処理をスキップするような内容になっていますね。

60~62行目は、printf関数の実行に該当する部分です。

64行目は、「jmp」命令で「$LN1@main」の位置までジャンプし、eelse部分をスキップしています。


続いて、最後のelse分のCソースコードとアセンブリコードを並べてみると下記の様になります。

	else
	{
		printf("aはbより小さい\n");
	}
; Line 18
	push	OFFSET $SG9256
	call	_printf
	add	esp, 4
$LN1@main:

67~69行目は、printf関数の実行に該当する部分です。

最後のelseブロックは、ブロック内の処理を行うコードのみとなっています。


今回は、値の比較だけでしたが、条件分岐は

  • 「cmp」命令で条件式を評価
  • 条件に一致しない場合「jle」、「jne」などのジャンプ命令で以降の処理をスキップ
  • 条件が一致した場合、ブロック内の処理を実行し、「jmp」命令で処理の実行が不要なコードをスキップ

の様な流れになっている事が分かりました。

処理時間の確認

条件分岐(if文)のコード自体は命令数が少ないので、下記の様な総和を計算するコードをベースに、if文を追加した場合の処理時間を計測するコードを用意しました。

#include <stdio.h>
#include <windows.h>

#define LOOP_NUM 100000000 // ループ回数:1億回

long sub(long a, long b)
{
	return a - b;
}

void main()
{
	long long sum1 = 0;
	long long sum2 = 0;
	long long sum3 = 0;
	long t0, t1, t2, t3;
	long v = 0;
	
	t0 = GetTickCount();
	// 総和計算
	for(long i = 1; i <= LOOP_NUM; i++)
	{
		sum1 += i;
	}
	t1 = GetTickCount();

	// 総和計算(if文:変数比較)
	for(long i = 1; i <= LOOP_NUM; i++)
	{
		if (i >= v)
		{
			sum2 += i;
		}
	}
	t2 = GetTickCount();

	// 総和計算(if文:関数呼び出し比較)
	for(long i = 1; i <= LOOP_NUM; i++)
	{
		if (sub(i, v) >= 0)
		{
			sum3 += i;
		}
	}
	t3 = GetTickCount();

	printf("sum1 = %lld, time = %ld\n", sum1, t1 - t0);
	printf("sum2 = %lld, time = %ld\n", sum2, t2 - t1);
	printf("sum3 = %lld, time = %ld\n", sum3, t3 - t2);
}

1つ目の総和計算は条件文なし(正確にはforループ内に条件文がありますが…)、2つ目の総和計算は変数比較するif文を追加したもの、3つ目の総和計算は関数の戻り値を比較するif文を追加したものとなります。(if文内は必ず真になるようにしています)

このコードをコンパイルして実行すると、下記の様な結果になりました。(計測時間は多少ばらつきがあります)

C:\Test\LLP_006>sample_006_2.exe
sum1 = 5000000050000000, time = 266
sum2 = 5000000050000000, time = 312
sum3 = 5000000050000000, time = 391

変数比較の条件文が追加されると、1~2割処理時間が延びています。

また、関数呼び出しと組み合わせた条件文が追加されると、4割ほど処理時間が増加しています。

元々の処理(総和の計算)自体、処理量が少ないので割合で比較すると多く見えますが、それでも、条件分岐をコードに加えると、処理時間は増加する事が分かります。

条件分岐の補足

条件分岐はその名前の通り、条件に応じて処理を分岐させます。

比較命令・ジャンプ命令が増えるだけとも言えますが、実際に処理時間にインパクトを与える場合があります。

CPUの理論的な動作としては、

  1. PC(プログラムカウンタ)の命令をプログラム領域から取得する
  2. 取得した命令を実行する。
  3. 次の命令へPCを更新する

の様になりますが、命令実行の度にメモリアクセスが発生すると、メモリアクセス速度に引っ張られCPUの処理速度が上がりません。

そのため『パイプライン処理』などで高速化を図っていますが、条件分岐が起きてしまうと、パイプライン処理の恩恵が受けられなくなってしまいます。(先読みしていたプログラムが無駄になるため)

今回のまとめ

今回は、条件分岐について確認してみました。

条件分岐のコード自体は、比較命令とジャンプ命令で組み合わされている事や、条件文が追加されるとs処理時間も若干長くなる事が確認出来ました。

これまで通り、単体の条件分岐だけでは、処理量・処理時間の増加量はわずかです。

ただ、色々な処理が組み合わされると、やはり処理時間は伸び、大きなデータを扱う場合には、「明らかに遅い」と思えるようになってきます。

次回の予定

次回はスタック領域とヒープ領域について確認していこうと思います。

前の記事次の記事
No.05:配列とポインタNo.07:スタック領域とヒープ領域