volatileで最適化を抑制する
プログラムは思った通りに動かない、書いた通りに動く。(プログラミングの格言)
C言語やC++,Javaにはvolatileという修飾子があります。組み込み系ソフトウェアやマルチスレッドのアプリケーションを書いている方にとっては、なじみ深い存在ですが、そうでない方にはあまり縁がないのかもしれません。しかし、volatileの使い方や存在意義を知らないままコーディングを行うと、思わぬバグを引き起こす場合があります。今回は、そのvolatileキーワードについて簡単に説明したいと思います。
volatileは初期のCであるK&Rには含まれていませんでしたが、ANSI C(C89)以降のC標準規格にはconstと一緒に含まれるようになりました(constとvolatileをあわせてcv修飾子と呼ぶこともあります)。一般的なCなら必ず備えている修飾子です。
volatile修飾子の意味ですが、「プログラミング言語C ANSI規格準拠
volatileの目的は,黙っていると処理系で行われる最適化を抑止することにある.例えば,メモリ・マップ方式の入出力をもつマシンでは,ステータス・レジスタに対するポインタは,ポインタによる見かけ上,冗長な参照をコンパイラが除去するのを防ぐのに,volatileへのポインタと宣言することが可能である.となっています。 なぜ最適化を抑止する必要があるのか?例をあげて説明します。
条件分削除の抑制
次のコードを見てください。変数の値が変化するまで、変数をポーリングし続けています。
extern int event_flag
void poll_event()
{
while (event_flag == 0) {
/* 処理。但しevent_flagは操作しない */
....
}
....
}
whileの続行条件としてグローバル変数event_flagの値を参照していますが、ループ内部でevent_flagの値を一切処理していません。よって、単純に見ると、このwhileの条件は毎回評価する必要なく、次のような最適化が可能となります。
void poll_event()
{
if (event_flag == 0) {
while (1) {
/* 処理。但しevent_flagは操作しない */
....
}
}
....
}
不要な条件評価を削除するのは最適化の基本であり、実際、大抵のコンパイラでは上記相当のコードが生成されます。このような最適化は処理速度の向上にとって大変有難いのですが、上記の関数は、本当にこのような最適化をしても良いのでしょうか?
シングルスレッドモデルの場合は、特に問題はありません。問題は、この変数が他のスレッドや、ハードウェアによって書き換えられる可能性がある場合です。つまり、上記のループはその関数をコールしているスレッド自身ではなく、割り込み処理や他のスレッドからの操作されることによって変化することを期待していたという場合です。
この場合は、上記のような最適化がなされた場合、途中で他スレッド、あるいは割り込み処理が値を変更したとしても、最適化のためにループから抜けることがなくなってしまいます。コンパイラは、現在の関数コンテクスト以外からその変数を操作することはないという前提にたって最適化を行っているため、このようなことが起こります。
そこで、このような複数スレッドからアクセスされる変数に対する最適化を抑制するために、volatile修飾子を使用します。具体的には、次の例のように、変数宣言にvolatile指定を追加します。
extern volatile int event_flagこれで、event_flagに対する最適化は抑制され、先ほどのような意図しない最適化も防ぐことが出来ます。
処理手順の保存
条件分岐の最適化以外にも、処理手順の最適化によって意図しない動きになることもあります。次の例を見てください。
extern int* p_regster1;
extern int* p_regster2;
void set_regester2(int val)
{
/*必ず次の手順でレジスタ設定する必要がある*/
*p_register1 = 1;
*p_register2 = 0;
*p_register2 = val;
*p_register1 = 0;
}
register2の値を設定する前に、regster1を一旦1にしており、register2設定後今度は0を設定しています。組み込みソフトウェアなどでは、HWの仕様上このような一見(Cコード上)無駄な手順が必要なことがあります。
これも一般的なコードとして見れば冗長なので、volatile指定無しの場合、最適化される可能性があります。例えば、次のようになります。
void set_regester2(int val)
{
*p_register2 = val;
*p_register1 = 0;
}
このような最適化は、単一のスレッドからしか呼ばれない関数でも起こりえます。回避策は、先ほどと同様、変数のvolatile指定をつけることです。
extern int* p_regster1; extern int* p_regster2;
近年のコンパイラの最適化能力はすばらしいので、アセンブラを強く意識したコードを書く必要はなくなってきました。しかし、最適化による影響を正しく理解していないと、上記のように予期せぬ不具合に見舞われることがあります。仮想関数、ガベッジコレクション等、言語レベルでの機能も高級化してきていますが、その根底にある動作をある程度把握していないと、最適なコードは書けません。
最適化に関しては、デバッグ時にデバッガが表示する変数の値に関する注意などもあるのですが、それはまた別の機会に。
【関連リンク】
・「組み込み」ならではの基礎知識 (組み込みネット)
・法大奥山研究室:C言語 volatile
・特集:スレッドの落とし穴 (ITmedia)
【関連書籍】
・実践マルチスレッドプログラミング Steve Kleiman
・Pthreadsプログラミング ブラッド・ニコルス 他
・CプログラミングFAQ―Cプログラミングのよく尋ねられる質問 Steve Summit 北野欽一
・Cプログラミングの落とし穴 Andrew Koenig 中村明
・C/C++による組み込みシステムプログラミング
・組み込みLinux入門―開発環境/デバイスドライバ/ミドルウェア/他OSからの移行
・ITRONプログラミング入門―組み込みOSのデファクト・スタンダード プログラミング詳細とサービス・コール徹底解説