1. コンパイラの最適化を防ぐ
コンパイラは、プログラムの実行効率を上げるために、見た目上冗長なコードを削除したり、最適化したりすることがあります。しかし、この最適化によって、特定の状況下で意図しない動作を引き起こすことがあります。volatileキーワードは、コンパイラに対して「この変数の値は予期せず変更される可能性があるため、最適化しないでください」と指示するために使用されます。
ハードウェアレジスタへのアクセス
並列デバイスのハードウェアレジスタ(例:I/Oポートやステータスレジスタ)は、プログラムの制御外で常に値が変化する可能性があります。例えば、ループ内でレジスタに連続して値を書き込む場合、コンパイラは「最終的な値しか意味がない」と判断し、ループを最適化してしまいます。
volatile uint32_t *led_control = (uint32_t *)0x40021000; // LED制御レジスタ
void toggle_leds(void) {
for (int i = 0; i < 8; ++i) {
*led_control = 1 << i; // 各ビットでLEDを点灯させる
}
}
上記のコードは、8つのLEDを順番に点灯させることを意図しています。しかし、volatile修飾子がない場合、コンパイラはループを最適化し、最後の命令だけを残してしまう可能性があります。その結果、ループは最後のLEDのみを点灯させるコードにコンパイルされてしまいます。
マルチスレッド環境での共有変数
複数のスレッド間で共有されるフラグ変数などは、volatile修飾子が必須です。一つのスレッドがフラグをセットし、別のスレッドがそのフラグを待機するような場合、コンパイラの最適化は深刻な問題を引き起こします。
volatile bool data_ready = false;
// データ受信を待つスレッド
void consumer_thread() {
while (!data_ready) {
// フラグがセットされるのを待つ
}
// data_readyがtrueになったら処理を続行
process_data();
}
// データ受信が完了したスレッド
void producer_thread() {
receive_data();
data_ready = true; // フラグをセット
}
上記の例では、consumer_threadはdata_readyがtrueになるのを待っています。もしdata_readyにvolatile修飾子がなく、コンパイラがこの変数をレジスタにキャッシュしてしまうと、producer_threadがdata_readyをtrueに設定しても、consumer_threadはキャッシュされた古いfalseの値を読み続け、永遠に待ち続けてしまいます。
2. キャッシュの整合性を確保する
コンパイラは、変数の読み書き速度を向上させるために、変数の値をCPUのレジスタやキャッシュメモリに一時的に保持することがあります。しかし、この最適化により、メモリ上の実際の値とキャッシュされた値の間に不整合が生じる問題が発生します。
例えば、ある変数が割り込みハンドラによって更新される場合、メインのプログラムはその変数を読み出す際に、常に最新の値をメモリから取得する必要があります。volatileキーワードは、このような場合に変数の読み書きを常にメモリに対して行うよう強制し、キャッシュの不整合を防ぎます。ただし、この操作はメモリアクセス回数が増えるため、パフォーマンスに影響を与える可能性があります。
割り込みハンドラによる変数の更新
タイマーやUARTなどの割り込みが発生するデバイスは、割り込みハンドラ内で共有変数を更新することがよくあります。この変数をメインループで監視する場合、volatile修飾子が不可欠です。
typedef struct {
uint32_t sensor_id;
volatile uint32_t sample_count; // 割り込みによって更新される
float latest_value;
} sensor_data_t;
sensor_data_t sensor;
void UART_IRQHandler(void) {
if (UART_GetITStatus(UARTx) == SET) {
// 新しいセンサデータを受信
sensor.latest_value = read_sensor_value();
sensor.sample_count++; // 受信カウンタをインクリメント
UART_ClearITPendingBit(UARTx);
}
}
void main_loop() {
while (1) {
if (sensor.sample_count > last_count) {
// sample_countが更新されたことを検出
process_sensor_data(&sensor);
last_count = sensor.sample_count;
}
// その他の処理
}
}
この例では、UART割り込みハンドラがセンサデータを受信するたびにsample_countをインクリメントします。メインループはこのカウンタの増加を監視して、新しいデータの処理を開始します。sample_countにvolatile修飾子がないと、コンパイラはこの変数をレジスタに保持し、メインループは常に古い値を読み出してしまい、新しいデータの到着を検出できなくなります。