学校では教えてくれないこと

コードが消える?~最適化の罠~

2007.6

コンパイラには、最適化オプションというのがあるのは知っているよね。最適化の種類も、コードサイズに関するものや動作速度に関するものなど、いろいろある。

でも、時としてコンパイラの最適化が悪さをして、組込みソフトウエアが正しく動かなくなってしまうことがあるんだ。

具体的な例をあげて説明してみよう。

例1

メモリアドレス0xB8000040に配置されている16-bitレジスタ(RegA)があるとする。このレジスタの最下位ビットがセットされるまで、ポーリングループで監視するコードを例にとってみよう。

コード例

これをコンパイルして実行すると、意図しない結果となってしまう。具体的に言うと、ポーリング監視処理が働かず、ビットがセットされていないのに、funcWhenBitSet()が呼ばれる可能性があるんだ。

どこがおかしいのだろうか?ロジック的には問題はないし、コンパイル時にエラーもないよね。

原因は、コンパイラの最適化なんだ。ポインタ変数regAは、while()でしか使われていない。また、実際にはregAの指すアドレスにはハードウエアのレジスタがあり、ビット値は変化するけど、ソースコード上は操作していないからregAの内容が「変化しない」ように見える。このため、コンパイラは最適化の過程で「regAは使われていない変数」という判断を下して、カットしてしまうんだ。

次に、この不具合をデバッグすることを考えてみよう。ポーリングがうまく働かないようなので、レジスタの値をログ表示させてみたくなるよね。

コード例

すると、ポーリングが動くようになる。あれ?なぜだかわかるかな?

これはprintf()で「regAが使われている」ことによって、最適化をかけてもポーリング処理が削除されなくなるからなんだ。「ログを削除すると不具合が起こるが、ログを追加すると起こらない」という非常にやっかいな(デバッグしづらい)パターンに陥ってしまう。

では、どうすればよいのだろうか。それは「変数regAを最適化してほしくない」ことを明示的に示せばいいよね。これをしてくれるのが「volatile修飾子」だ。constなどと同様に、ANSI規格のC言語で定義されている修飾子で「コンパイラによる最適化を抑止する」というものなんだ。

以下のように変数regAを宣言するところにvolatileを追加する。

コード例

こうするとコンパイラは変数regAについて最適化を行わなくなり、ポーリングが正常に動作するようになるんだ。 もうひとつ別の例を見てみよう。

例2

0xB0000050に配置されているデータ書き込み用8-bitレジスタ(RegB)があるとする。さらにデータシートに、このレジスタは一度書き込んでから次に書き込むまでに一定時間ウェイトする必要がある、と注意書きが書かれていたとしよう。これを以下のようにコーディングしてみるよ。

コード例

変数regBの宣言にはちゃんとvolatileがついているよね。これはOKだ。しかし、これではまだ問題があるんだ。わかるかな?

それは、forの空ループなんだ。最初の例でも説明したけど、変数iはこのループでしか使われていない。コンパイラは、他の箇所で参照されていない変数のループだから意味がないとみなして削除してしまうんだ。その結果、想定したウェイトが得られずデータが正しく書き込めないという結果になってしまう。

このように、volatile修飾子は組込みソフトウエアでは非常に重要なんだ。ハードウエアレジスタに対するアクセスを行う変数には必ずつけておく必要があるよ。

補足

ちなみに、(例2)の空ループによるウェイトは好ましい使い方ではない。なぜなら、CPUの処理速度によって確保できるウェイト時間が変動してしまうからだ。

あるシステムでは動いても、他のシステムに移植したときに正しく動かなくなるのでは困ってしまうからね。でも、次のレジスタ書き込みまでの区間は、タスク切替えを禁止したい、さらにOSのシステムコール呼び出しもしたくない、といった特殊な場合には、あえてこのような空ループを使うこともあるんだ。

ページのトップへ