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

どうも、ながやすです。

最近のパソコンでは、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 より:

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