5.8 コンパイラの最適化と volatile キーワード

高い最適化レベルを使用した場合、例えば volatile 修飾子が欠けているなど、低い最適化レベルを使用した場合には発生しないプログラムの問題が明らかになることがあります。

このような問題は、いろいろな形で明らかになります。例えば、ハードウェアをポーリングする際にループから抜けられなくなったり、マルチスレッドコードによって異常な動作が示されたりする場合があります。また、最適化によって、意図的なタイミングの遅延を実装するコードが削除されることもあります。このような場合は、一部の変数を volatile として宣言することが必要となります。
変数を volatile として宣言すると、その変数を実装の外部で(例えば、オペレーティングシステムによって、割り込みルーチンやシグナルハンドラなどの実行の別のスレッドによって、またはハードウェアによって)いつでも変更できることが、コンパイラに伝達されます。 volatile で修飾された変数の値はいつでも変化する可能性があるので、変数がコードで参照されるたびに、メモリ内の実際の変数にアクセスする必要があります。つまり、コンパイラは、変数の値をレジスタにキャッシュしてメモリアクセスを防ぐなど、変数に対する最適化を実行できません。同様に、スリープまたはタイマ遅延を実装するコンテキストで変数を volatile として宣言すると、特定の種類の動作が意図的なものであり、意図された機能が除去されるような結果となる該当するコードの最適化を行わないことがコンパイラに通知されます。
一方、変数が volatile として宣言されていない場合、コンパイラはその値を予期しない方法で変更できないものと見なすことができます。そのため、コンパイラは変数に対する最適化を実行できます。
volatile キーワードの使用法を、以下の表の 2 つのサンプルルーチンに示します。いずれのルーチンも、ステータスフラグ buffer_full が true に設定されるまで、バッファの読み出しを続けます。buffer_full の状態は、プログラムフローとは非同期に変化する可能性があります。
ルーチンの 2 つのバージョンで異なる点は、buffer_full の宣言だけです。1 つ目のルーチンバージョンは不適切です。このバージョンでは、変数 buffer_full volatile が指定されていません。一方、2 番目のルーチンバージョンは、同じループを示していますが、buffer_full volatile が正しく指定されています。

表 5-5 非揮発バッファループと揮発バッファループを表す C コード

バッファループの非揮発バージョン バッファループの揮発バージョン
int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}
volatile int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}
以下の表は、コンパイラによって生成されたマシンコードが上の各例でどのように逆アセンブルされるかを示したものです。いずれの実装の C コードも、オプション -O2 を使用してコンパイルされています。

表 5-6 非揮発バッファループと揮発バッファループを表す逆アセンブリコード

バッファループの非揮発バージョン バッファループの揮発バージョン
read_stream PROC
    LDR      r1, |L1.28|
    MOV      r0,#0
    LDR      r1, [r1, #0]
|L1.12|
    CMP      r1, #0
    ADDEQ    r0, r0, #1
    BEQ      |L1.12|      ; 無限ループ
    BX       lr
    ENDP
|L1.28|
    DCD      ||.data||
    AREA ||.data||, DATA, ALIGN=2
buffer_full
    DCD      0x00000000
read_stream PROC
    LDR      r1, |L1.28|
    MOV      r0,#0
|L1.8|
    LDR      r2, [r1, #0];  ; buffer_full
    CMP      r2, #0
    ADDEQ    r0, r0, #1
    BEQ      |L1.8|
    BX       lr
    ENDP
|L1.28|
    DCD      ||.data||
    AREA ||.data||, DATA, ALIGN=2
buffer_full
    DCD      0x00000000
上の表に示すバッファループの非揮発バージョンの逆アセンブリでは、ステートメント LDR r0, [r0, #0] によって、|L1.12| というループ外部のレジスタ r0buffer_full の値がロードされます。buffer_full volatile として宣言されていないので、コンパイラはその値をプログラムの外部で変更できないものと見なします。コンパイラは、buffer_full の値を r0 に既に読み込んでおり、この変数の値が変化する可能性はないので、最適化が有効にされている場合は、変数の再ロードを省略します。その結果、|L1.12| という無限ループが生成されます。
それとは逆に、バッファループの揮発バージョンの逆アセンブリでは、buffer_full の値がプログラムの外部で変化する可能性があるものとコンパイラによって見なされ、最適化は実行されません。その結果、buffer_full の値は、|L1.8| というループの内部にあるレジスタ r0 にロードされ、ループ |L1.8| がアセンブリコードで正しく実装されます。
プログラムの状態が実装の外部で変更されたことに起因する最適化の問題を防ぐには、実装で認識されていない方法で変数の値が予期せず変化する可能性がある場合は必ず、変数を volatile として宣言する必要があります。
実際の環境では、以下の場合に変数を volatile として宣言する必要があります。
  • メモリマップされたペリフェラルにアクセスする場合。
  • グローバル変数を複数のスレッド間で共有する場合。
  • 割り込みルーチンまたはシグナルハンドラでグローバル変数にアクセスする場合。
volatile として宣言した変数は、コンパイラによって最適化されません。
非機密扱いPDF file icon PDF 版ARM DUI0472LJ
Copyright © 2010-2015 ARM.All rights reserved.