C言語のループ処理を3種類の書き方でアセンブラに変換し、どの書き方が効率の良い機械語を生成するか比較します。


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

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

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

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

これを、オブジェクトファイルでなくアセンブラのコードに変換するにはgcc -O2 -S hoge.c のように-S オプションを使います。すると .sのアセンブラファイルが生成されます。

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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命令ですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    .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ループで書いてみます。たまに「デクリメントループは読みにくいからやめるべし」という方もいますが、まあ、組込み屋ならパッとみて理解できた方が良いかなと思います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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命令に減りましたね!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    .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ループで書いてみます。 これは組込屋さんでもぱっと見では少しわかりづらいかもしれません。 コメントで「高速化のためにあえてやっている」という旨を書いておくといいかもしれません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#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命令から比べるとだいぶ減りました!!!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    .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言語ソースコードを最適化する例でした!

Note

簡単にアセンブラを確認する方法を 超便利!C言語のアセンブラ出力を即確認できるサイト|compiler-explorerに書きました!あわせてご参照ください!