組み込み機器向けのソースコードでとても重要になるキーワード、それがvolatileです。volatileとは必ずメモリアクセスをともなうコードを生成する、という意味です。コンパイラの仕事の一つに最適化があります。その名の通り、プログラムを最適な状態にする、ロジックを変えることなく無駄を省いて効率の良いプログラムを生成します。
volatileをつけると何が変わるのか?
たとえばこのようなコードがあったとします。
int i;
volatile int vi;
i = 1;
i = 2;
printf("%d\n", i);
vi = 3;
vi = 4;
printf("%d\n", vi);
まずは最適化なしでコンパイルするとこのようなコードが生成されます。gcc -S を使うとどのようあんアセンブリコードになるのかを見ることができます。
movl $1, -20(%rbp)
movl $2, -20(%rbp)
movl -20(%rbp), %esi
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
movl $3, -24(%rbp)
movl $4, -24(%rbp)
movl -24(%rbp), %esi
leaq L_.str(%rip), %rdi
movl %eax, -28(%rbp) ## 4-byte Spill
movb $0, %al
callq _printf
-20($rbp)が変数iで、-24(%rbp)が変数viですね。iに1を代入して2を代入してprintfを呼び出し、viに3を代入して4を代入してprintfを呼び出し、というC言語そのままといえばそのままのコードです。printfに渡す引数iの値は-20($rbp)から読み出して%esiに入れて渡されています。
次に、これをgcc -O3 -Sとして最適化します。
movl $2, %esi
xorl %eax, %eax
callq _printf
movl $3, -12(%rbp)
movl $4, -12(%rbp)
movl -12(%rbp), %esi
movq %rbx, %rdi
xorl %eax, %eax
callq _printf
コードが最適化されたことによって、無駄なことはなくなりました。無駄だったこととは何か?
- 変数iに1を代入してもすぐに2を代入するので、1を代入するのは無駄
- 変数iに2を代入してもprintfに渡すのは%esiレジスタなので変数に代入するのは無駄
ということで、iの方は$rbpがなくなり、1もなくなって、いきなり2を%esiに入れてprintfを呼び出しています。では、viの方はというと、最適化したせいで番地は変わりましたが-12(%rbp)として残っていて、最適化なしの場合とまったく変化なしです。無駄に1を代入して2を代入して、そこから読み出してレジスタにいれてprintfに渡しています。このロジックの場合は完全に無駄ですね。
volatileが役立つ時とは?
組み込み機器で使うさまざまなペリフェラル機能は、メモリマップドIOと言われ、メモリのように番地が割り当てられて読み書きできるようになっています。AD変換した結果の読み出し、UARTでの送受信、DMAの設定、なんでもメモリアドレスを読み書きして行います。UARTに文字列を1文字ずつ出力する場合、ソフトウェアの観点だけからプログラムのロジックを考えると、最後の1文字だけ書き込めば良いだけですし、さらに言うとメモリアクセスをせずとも最後の1文字だけをレジスタに入れれば良いだけかもしれませんし、もっと言うと何もしなくてもソフトウェアのロジックとしては最適かもしれません。ハードウェアとのやりとりが関わる場合、volatileをつけることで最適化を抑制して必ずメモリアクセスを伴うコードを生成するのです。
コメント