関数内で使うローカルな文字列テーブル(配列)の定義には注意が必要です。
1月~12月までの月を数値で渡すと、英語の文字列を取得できる関数monthToEnglishを作ってみましょう。
例.第一引数に5を渡すとMayという文字列が第二引数のバッファに格納される
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 |
#include <cstring> constexpr int BUF_SIZE = 32; void monthToEnglish( int month, char* buf ) { const char* MONTH_ENGLISH_TABLE[12] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", }; std::strncpy(buf, MONTH_ENGLISH[month-1], BUF_SIZE); } int main() { char buf[BUF_SIZE]; monthToEnglish(3, buf); return 0; } |
無事、完成しました。
有効範囲のエラー処理など細かい部分は端折ってますが適切な値を渡す限りは要件に見合った挙動をします。
さてこの関数ですが、大いなる無駄処理が走る可能性があります。
最適化を有効にした上で、アセンブラを見てみましょう。
1 |
g++ main.cpp -S -std=c++11 -O3 |
41行目~に注目です。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
.file "main.cpp" .section .rdata,"dr" .LC0: .ascii "January\0" .LC1: .ascii "February\0" .LC2: .ascii "March\0" .LC3: .ascii "April\0" .LC4: .ascii "May\0" .LC5: .ascii "June\0" .LC6: .ascii "July\0" .LC7: .ascii "August\0" .LC8: .ascii "September\0" .LC9: .ascii "October\0" .LC10: .ascii "November\0" .LC11: .ascii "December\0" .text .p2align 4,,15 .globl _Z14monthToEnglishiPc .def _Z14monthToEnglishiPc; .scl 2; .type 32; .endef .seh_proc _Z14monthToEnglishiPc _Z14monthToEnglishiPc: .LFB1213: subq $104, %rsp .seh_stackalloc 104 .seh_endprologue leaq .LC0(%rip), %rax movl $32, %r8d movq %rdx, %r9 movq %rax, (%rsp) leaq .LC1(%rip), %rax movq %rax, 8(%rsp) leaq .LC2(%rip), %rax movq %rax, 16(%rsp) leaq .LC3(%rip), %rax movq %rax, 24(%rsp) leaq .LC4(%rip), %rax movq %rax, 32(%rsp) leaq .LC5(%rip), %rax movq %rax, 40(%rsp) leaq .LC6(%rip), %rax movq %rax, 48(%rsp) leaq .LC7(%rip), %rax movq %rax, 56(%rsp) leaq .LC8(%rip), %rax movq %rax, 64(%rsp) leaq .LC9(%rip), %rax movq %rax, 72(%rsp) leaq .LC10(%rip), %rax movq %rax, 80(%rsp) leaq .LC11(%rip), %rax movq %rax, 88(%rsp) movslq %ecx, %rax movq %r9, %rcx movq (%rsp,%rax,8), %rdx addq $104, %rsp jmp strncpy .seh_endproc .def __main; .scl 2; .type 32; .endef .section .text.startup,"x" .p2align 4,,15 .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: .LFB1214: subq $72, %rsp .seh_stackalloc 72 .seh_endprologue call __main leaq 32(%rsp), %rdx movl $3, %ecx call _Z14monthToEnglishiPc xorl %eax, %eax addq $72, %rsp ret .seh_endproc .p2align 4,,15 .def _GLOBAL__sub_I__Z14monthToEnglishiPc; .scl 3; .type 32; .endef .seh_proc _GLOBAL__sub_I__Z14monthToEnglishiPc _GLOBAL__sub_I__Z14monthToEnglishiPc: .LFB1234: subq $40, %rsp .seh_stackalloc 40 .seh_endprologue leaq _ZStL8__ioinit(%rip), %rcx call _ZNSt8ios_base4InitC1Ev movq .refptr.__dso_handle(%rip), %r8 leaq _ZStL8__ioinit(%rip), %rdx leaq _ZNSt8ios_base4InitD1Ev(%rip), %rcx addq $40, %rsp jmp __cxa_atexit .seh_endproc .section .ctors,"w" .align 8 .quad _GLOBAL__sub_I__Z14monthToEnglishiPc .lcomm _ZStL8__ioinit,1,1 .ident "GCC: (GNU) 4.9.2" .def strncpy; .scl 2; .type 32; .endef .def _ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef .def _ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef .def __cxa_atexit; .scl 2; .type 32; .endef .section .rdata$.refptr._ZNSt8ios_base4InitD1Ev, "dr" .globl .refptr._ZNSt8ios_base4InitD1Ev .linkonce discard .refptr._ZNSt8ios_base4InitD1Ev: .quad _ZNSt8ios_base4InitD1Ev .section .rdata$.refptr.__dso_handle, "dr" .globl .refptr.__dso_handle .linkonce discard .refptr.__dso_handle: .quad __dso_handle |
MONTH_ENGLISH_TABLEはこの関数が呼び出される度にスタック上にコピーされます。ちなみにconst を constexprに変えても出力結果は同じです。
スタック上に値を生成したいという目的が無いのならば、このコードはこの関数が呼ばれる度にただただ不要な処理を走らせることになるのであまりよろしくありません。
例えばテクスチャ名のリスト、モーション名のリスト、気軽にローカルで定義することは良くあるかと思います。
そういう時に、gcc 4.9.2 で -O3ですら、前述のような(プログラマの意図に対して)無駄な処理が走ることは覚えておく価値があると思います。
この問題は定義にstaticを付けることで簡単に回避できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static const char* MONTH_ENGLISH_TABLE[12] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", }; |
この場合のアセンブラコードを見てみましょう。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
.file "main.cpp" .text .p2align 4,,15 .globl _Z14monthToEnglishiPc .def _Z14monthToEnglishiPc; .scl 2; .type 32; .endef .seh_proc _Z14monthToEnglishiPc _Z14monthToEnglishiPc: .LFB1213: .seh_endprologue movl $32, %r8d movq %rdx, %r9 leaq _ZZ14monthToEnglishiPcE19MONTH_ENGLISH_TABLE(%rip), %rdx movslq %ecx, %rax movq %r9, %rcx movq (%rdx,%rax,8), %rdx jmp strncpy .seh_endproc .def __main; .scl 2; .type 32; .endef .section .text.startup,"x" .p2align 4,,15 .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: .LFB1214: subq $72, %rsp .seh_stackalloc 72 .seh_endprologue call __main xorl %eax, %eax addq $72, %rsp ret .seh_endproc .p2align 4,,15 .def _GLOBAL__sub_I__Z14monthToEnglishiPc; .scl 3; .type 32; .endef .seh_proc _GLOBAL__sub_I__Z14monthToEnglishiPc _GLOBAL__sub_I__Z14monthToEnglishiPc: .LFB1234: subq $40, %rsp .seh_stackalloc 40 .seh_endprologue leaq _ZStL8__ioinit(%rip), %rcx call _ZNSt8ios_base4InitC1Ev movq .refptr.__dso_handle(%rip), %r8 leaq _ZStL8__ioinit(%rip), %rdx leaq _ZNSt8ios_base4InitD1Ev(%rip), %rcx addq $40, %rsp jmp __cxa_atexit .seh_endproc .section .ctors,"w" .align 8 .quad _GLOBAL__sub_I__Z14monthToEnglishiPc .section .rdata,"dr" .LC0: .ascii "January\0" .LC1: .ascii "February\0" .LC2: .ascii "March\0" .LC3: .ascii "April\0" .LC4: .ascii "May\0" .LC5: .ascii "June\0" .LC6: .ascii "July\0" .LC7: .ascii "August\0" .LC8: .ascii "September\0" .LC9: .ascii "October\0" .LC10: .ascii "November\0" .LC11: .ascii "December\0" .align 64 _ZZ14monthToEnglishiPcE19MONTH_ENGLISH_TABLE: .quad .LC0 .quad .LC1 .quad .LC2 .quad .LC3 .quad .LC4 .quad .LC5 .quad .LC6 .quad .LC7 .quad .LC8 .quad .LC9 .quad .LC10 .quad .LC11 .lcomm _ZStL8__ioinit,1,1 .ident "GCC: (GNU) 4.9.2" .def strncpy; .scl 2; .type 32; .endef .def _ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef .def _ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef .def __cxa_atexit; .scl 2; .type 32; .endef .section .rdata$.refptr._ZNSt8ios_base4InitD1Ev, "dr" .globl .refptr._ZNSt8ios_base4InitD1Ev .linkonce discard .refptr._ZNSt8ios_base4InitD1Ev: .quad _ZNSt8ios_base4InitD1Ev .section .rdata$.refptr.__dso_handle, "dr" .globl .refptr.__dso_handle .linkonce discard .refptr.__dso_handle: .quad __dso_handle |
見違えるほどスッキリしました。
このように、関数ローカルな場所に文字列テーブルを作る時は呼吸をするようにstaticを付けるというプラクティスは悪くありません。
まとめ
- ローカルな文字列テーブルの定義は無駄なコストが生じる可能性がある
- ローカルな文字列テーブルにはstaticをつけよう