C言語 Advent Calendar 2016を書く人が少ないので2日目に参加してみました。
C言語 Advent Calendar 2016
東京都在住のHさんからの質問です。
―――――――――――――――――――――――――
switch文ているんやろか。ダラダラ続いて分解しにくいif文って感じがして・・・。離脱もちょっとわかりにくいし。
―――――――――――――――――――――――――
自分の試した範囲では、switchが使いにくいと思うのであれば、特に使う必要はないという結論になりました。なぜならば、ifでもswitchでも結局は同じアセンブリが出力されるから。
◇ 昔は
昔は、switch文はパフォーマンス的に必要でした。というのもif文はifの数だけ条件分岐します。そのため100個のifがあると100回判定してしまいます。
しかし、switch文はcaseの数が増えるとジャンプテーブルで1発でジャンプできる等の最適化がかかります。
ジャンプテーブルについて
そのためパフォーマンスを意識した場合にifとswitchを書き分ける必要がありました。
◇ 今は
では、今はどうでしょう。
以下のプログラムでアセンブリを比べます。
①switch文のプログラム
#includeint main() { int r; int n; scanf("%d",&n); switch (n) { case 0: r = 10; break; case 1: r = 20; break; case 2: r = 12; break; case 3: r = 14; break; case 4: r = 19; break; default: r = -1; } printf("r:%d",r); return 0; }
②if文のプログラム
#includeint main() { int r; int n; scanf("%d",&n); if (n==0) { r = 10; } else if (n == 1) { r = 20; } else if (n == 2) { r = 12; } else if (n == 3) { r = 14; } else if (n == 4) { r = 19; } else { r = -1; } printf("r:%d",r); return 0; }
③上記のプログラムのアセンブリ
上記の①、②の2つのプログラムはまったく一緒のアセンブリとなりました(※)
(diffを取ったところ全く同じでした。)
このプログラムだとジャンプテーブルすら作ってなく、データのテーブルを作って値を持ってきています。
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 11 .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 subq $16, %rsp leaq L_.str(%rip), %rdi leaq -4(%rbp), %rsi xorl %eax, %eax callq _scanf movslq -4(%rbp), %rax cmpq $4, %rax movl $-1, %esi ja LBB0_2 ## BB#1: ## %switch.lookup leaq l_switch.table(%rip), %rcx movl (%rcx,%rax,4), %esi LBB0_2: leaq L_.str.1(%rip), %rdi xorl %eax, %eax callq _printf xorl %eax, %eax addq $16, %rsp popq %rbp retq .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "%d" L_.str.1: ## @.str.1 .asciz "r:%d" .section __TEXT,__const .align 4 ## @switch.table l_switch.table: .long 10 ## 0xa .long 20 ## 0x14 .long 12 ## 0xc .long 14 ## 0xe .long 19 ## 0x13 .subsections_via_symbols
◇ 結論
switchとifどっちで書いても結果は一緒。読みやすい方を使えばいい。
◇ おまけ
上記を配列で書いたらどうなるでしょうか?
結局のところアセンブリ上、少し違うものの内容は同じ処理となり、結局は好きな書き方を使えば良い。
④配列でのプログラム
#includeint main() { int r; int n; scanf("%d",&n); int arr[] = { 10,20,12,14,19 }; if (n < 0 || n > 4) { //2016.12.14 コメントの指摘によりn > 5を修正。 r = -1; } else { r = arr[n]; } printf("r:%d",r); return 0; }
⑤上記のアセンブリ
.section __TEXT,__text,regular,pure_instructions .macosx_version_min 10, 11 .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 subq $16, %rsp leaq L_.str(%rip), %rdi leaq -4(%rbp), %rsi xorl %eax, %eax callq _scanf movslq -4(%rbp), %rax cmpq $4, %rax movl $-1, %esi ja LBB0_2 ## BB#1: leaq l_main.arr(%rip), %rcx movl (%rcx,%rax,4), %esi LBB0_2: leaq L_.str.1(%rip), %rdi xorl %eax, %eax callq _printf xorl %eax, %eax addq $16, %rsp popq %rbp retq .cfi_endproc .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "%d" .section __TEXT,__const .align 4 ## @main.arr l_main.arr: .long 10 ## 0xa .long 20 ## 0x14 .long 12 ## 0xc .long 14 ## 0xe .long 19 ## 0x13 .section __TEXT,__cstring,cstring_literals L_.str.1: ## @.str.1 .asciz "r:%d" .subsections_via_symbols
◇ 最後に
単純な例では、ifでもswitchでもarrayでも、一緒という結果になりました。
他の例でも一緒であると現状、思っています。
もし、switchで書けるもので最適化後のアセンブリでswitch有利、if有利な反例があればそのプログラムをお教えいただければありがたいです。
(※)アセンブリはclangで生成してます。
Apple LLVM version 8.0.0 (clang-800.0.42.1)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
また、最適化O2でa.cをプログラムとして
clang -save-temps a.c -O2
とコンパイルしています。
コメント
コメント一覧 (1)
・テーブル参照をするコードをコンパイラに吐かせたいのであればその意図はコンパイラに伝えるよう心掛けるべきでしょう。その点からは『④配列でのプログラム』がコンパイラの種類やバージョン、コンパイルオプションに係わらず意図したコードを出力させる目的には適っています。
・『④配列でのプログラム』は範囲検査が誤っており、5 が入力された場合には配列の範囲外を参照してしまいます。このようなミスを未然に防ぎたいという点では、switch ~ case の書き方は書式が定型化している点で優れています。if ~ else if ~ else の書き方は自由度が高く、他のミスも起こりやすいですね。
・『④配列でのプログラム』は arr[] の記憶クラスを static const に指定すべきでしょう。これがないとコンパイラによってはスタックフレーム上に arr[] を作成するため非効率なコードが吐かれる結果となります。プログラマの意図はコンパイラに伝わるよう心掛けましょう。
『④配列でのプログラム』の arr[] の型を char 等にすることで arr[] のサイズを縮小することが可能です。『①switch文のプログラム』や『②if文のプログラム』では arr[] のひとつの要素について 4バイトの領域が使用されており、メモリ効率の点では最適ではありません。