C言語のコードをアセンブラ出力を確認しながら最適化する

この記事は約7分で読めます。

最近のパソコンでは、CPUは高速でメモリもたくさんあるので、プログラマが頑張って高速な機械語を吐くように意識したコードを書くことはあまりありません。ですが、組み込みの世界ではまだまだCPUもメモリも貧弱な環境がたくさんあります。高速なCPUや大きいメモリはそのまま原価に跳ね返るためです。

そこで今回は、高速な機械語を吐くC言語のソースコードを書く練習をしてみます。やり方は、3種類の100回ループするC言語のソースコードを、それぞれアセンブラに変換して、どのくらい効率が良くなるか比較してみます。

スポンサーリンク

コンパイルしたコードのアセンブラの確認

C言語のソースコードをコンパイルすると、通常はオブジェクトファイル(.o)に変換されます。
$ gcc -O2 -c hoge.c -o hoge.o

これを、オブジェクトファイルでなくアセンブラのコードに変換するには下記のように -S オプションを使います。すると .s のアセンブラファイルが生成されます。(gcc 以外のコンパイラでも大体 -S のことが多いです、コンパイラのマニュアルで確認してみてください)
$ gcc -O2 -S hoge.c

また、gccなら下記の形でもOKです。これはアセンブラ .s ファイル以外に、プリプロセス済みファイルの .iファイルも生成する優れものです。
$ gcc -O2 -save-temps hoge.c

今回はこの -save-temps オプションを使用します。

スポンサーリンク

インクリメントなforループ

一般的なforループで書いてみます。

#define LOOP_NUM (100)

int main(void)
{
    volatile int i;

    for (i = 0; i < LOOP_NUM; i++)
        ;

    return 0;
}

このコードを
$ gcc -O2 -save-temps for_incr.c
にてコンパイルすると、下記のようなアセンブラが出力されます。13命令ですね。

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 12
	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	movl	$0, -4(%rbp)
	movl	-4(%rbp), %eax
	cmpl	$99, %eax
	jg	LBB0_2
	.align	4, 0x90
LBB0_1:                                 ## %.lr.ph
                                        ## =>This Inner Loop Header: Depth=1
	incl	-4(%rbp)
	movl	-4(%rbp), %eax
	cmpl	$100, %eax
	jl	LBB0_1
LBB0_2:                                 ## %._crit_edge
	xorl	%eax, %eax
	popq	%rbp
	retq
	.cfi_endproc


.subsections_via_symbols

スポンサーリンク

デクリメントなforループ

次はデクリメントなforループで書いてみます。たまに「デクリメントループは読みにくいからやめるべし」という方もいますが、まあ、組込み屋ならパッとみて理解できた方が良いかなと思います。

#define LOOP_NUM (100)

int main(void)
{
    volatile int i;

    for (i = LOOP_NUM; i; i--)
        ;

    return 0;
}

このコードを
$ gcc -O2 -save-temps for_decr.c
にてコンパイルすると、下記のようなアセンブラが出力されます。10命令に減りましたね!

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 12
	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	movl	$100, -4(%rbp)
	jmp	LBB0_2
	.align	4, 0x90
LBB0_1:                                 ## %.lr.ph
                                        ##   in Loop: Header=BB0_2 Depth=1
	decl	-4(%rbp)
LBB0_2:                                 ## %.lr.ph
                                        ## =>This Inner Loop Header: Depth=1
	cmpl	$0, -4(%rbp)
	jne	LBB0_1
## BB#3:                                ## %._crit_edge
	xorl	%eax, %eax
	popq	%rbp
	retq
	.cfi_endproc


.subsections_via_symbols
スポンサーリンク

デクリメントなdo-whileループ

次はデクリメントなdo-whileループで書いてみます。これは組込屋さんでもぱっと見では少しわかりづらいかもしれません。コメントで「高速化のためにあえてやっている」という旨を書いておくといいかもしれません。

#define LOOP_NUM (100)

int main(void)
{
    volatile int i = LOOP_NUM;

    do {
        ;
    } while (--i);

    return 0;
}

このコードを
$ gcc -O2 -save-temps do_while.c
にてコンパイルすると、下記のようなアセンブラが出力されます。8命令に減りましたね!素朴なforインクリメントループの13命令から比べるとだいぶ減りました!!!

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 12
	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	movl	$100, -4(%rbp)
	.align	4, 0x90
LBB0_1:                                 ## =>This Inner Loop Header: Depth=1
	decl	-4(%rbp)
	jne	LBB0_1
## BB#2:
	xorl	%eax, %eax
	popq	%rbp
	retq
	.cfi_endproc


.subsections_via_symbols
スポンサーリンク

まとめ

今回はループ文を例に、高速な機械語を吐くようなC言語のコードを書いてみました。このように、実際にC言語のソースコードを修正してアセンブラを確認して、、、というのを繰り返してコードを最適化できます。組み込みでは1Byte単位の勝負になることもあるので、この方法は頭の片隅に置いておくと良いかと思います。

以上、アセンブラを確認しながらC言語ソースコードを最適化する例でした!

コメント

  1. buchio より:

    https://godbolt.org/

    オンラインでいろんなコンパイラのASM出力を確認できるサイトです
    便利ですよー

    • nagayasu-shinya より:

      おおお、すごいですねこれ!!お教えいただきありがとうございます!!!!

  2. takky より:

    なぜ、このようにサイクル数が減るのか、解説頂けないでしょうか。

    • nagayasu-shinya より:

      最適化アルゴリズよりも最適化のためにアセンブラ出力を出して比較する方法がメインなのであまり深くは立ち入らないようにしてましたが、一応簡単に説明いたします。
      (最適化はCPUや処理系に思いっきり依存するので、一般的な事例をあげづらい。。。)

      CPUによって挙動が違うのでここではARMと仮定して説明します。

      ▪️インクリメントループをデクリメントループの違い:

      インクリメントループには3つのステップが必要です。
      1. ループカウンタの加算
      2. ループカウンタがループ回数未満かどうかの比較
      3. (ループ回数未満だった場合の)ループ先頭へのジャンプ

      一方、デクリメントループは下記の2つのステップでいけます。
      1. ループカウンタの減算&結果が0かどうかを条件フラグにセット
      2. 条件フラグがセットされていた場合の分岐

      ポイントは、減算命令は実行結果が0かどうかを条件フラグにセットするということです。これによりインクリメントループのような比較処理が不要になります。

      デクリメントループにすることによってループ終了条件を0との比較にすることができます。
      またCPUは減算命令の時にその結果が0かどうかを条件フラグにセットできます。

      ▪️for文とdo-while文の違い:

      for文は開始時に一回、ループカウンタとループ回数の比較が必要ですが、
      do-while はそれが必要ありません。そのためより最適化しやすくなります。

      こっちの例はちょっとイマイチでしたね。。。