メモリバリア

メモリバリア（英: Memory Barrier）またはメモリフェンス（Memory Fence）とは、その前後のメモリ操作の順序性を制限するCPUの命令の一種である。
CPUには、性能最適化策としてアウト・オブ・オーダー実行を行うものがあり、メモリのロード命令やストア命令を含めて順序を入れ替えて実行する。この命令の並べ替えは、ひとつのスレッドの中で一般に暗黙のうちに行われるが、マルチスレッドプログラムやデバイスドライバでは慎重に制御しない限り予測不能の動作を生じる原因となる。順序性の制限の方法はハードウェア依存であり、そのアーキテクチャによって定義される。アーキテクチャによってはいくつかのバリアを用意して、それぞれ異なった順序性制限を実現している場合がある。
メモリバリアは低レベルの機械語で使われることが多く、複数のデバイスが共有するメモリを操作するのに使われる。そのようなコードとして、同期プリミティブ、マルチプロセッサシステムでのロックフリーなデータ構造、何らかのハードウェア機器を制御するデバイスドライバなどがある。

簡単な例

プログラムが1個のCPUで動作している場合、ハードウェアは全てのメモリ操作がプログラムされた順番通りに行われたかのように見えるよう命令を実行する。従ってメモリバリアは不要である。しかし、メモリが複数の機器によって共有されている場合（マルチプロセッサシステムのCPU群や、メモリマップドI/Oなど）、アウトオブオーダー実行によってプログラムの結果が変わってしまうことがある。例えば、2番目のCPUから見て、1番目のCPUが行ったメモリ操作はプログラム上の順番と違って見えるかもしれない。
以下の2プロセッサプログラムは、アウトオブオーダー実行によってプログラムの動作が影響される具体例である。
まず、メモリ位置 x と f の値は共に 0 であるとする。プロセッサ#1 で動作するプログラムは f の値がゼロ以外になるまでループし、その後 x の値を表示する。プロセッサ#2 で動作するプログラムは x に 42 を格納してから f に 1 を格納する。擬似コードの一部を以下に示す。プログラムの各行が個々のプロセッサの命令に対応している。

プロセッサ#1:
 loop:
  位置 f の値をロードし、0 ならば loop へ分岐 位置 x の値を表示

プロセッサ#2:
 位置 x に 42 を格納 位置 f に 1 を格納
表示されるのは常に 42 であることが期待されているが、プロセッサ#2 のストア命令がアウトオブオーダーで実行されれば、f が x の前に更新される可能性があり、"0" が表示される可能性も出てくる。多くのプログラムではこのような状況は受け入れられない。メモリバリアをプロセッサ#2の位置 f へのストア命令の前に挿入すると、他のプロセッサから見ても x が f の前に更新されているように観測されることを保証できる。

低レベルアーキテクチャ向けのプリミティブ

メモリバリアは、アーキテクチャのメモリモデルの定義の一部の低レベルプリミティブである。命令セットと同様、メモリモデルはアーキテクチャによって様々であるため、その動作を概括的に述べることは適切ではない。一般にメモリバリアを正しく使用するには、プログラミング対象のハードウェアのアーキテクチャマニュアルを読むべきである。とはいうものの、以下ではいくつかの実在するメモリバリアについて紹介する。
いくつかのアーキテクチャでは、「フルフェンス」と呼ばれる一種類のメモリバリア命令だけを提供する。フルフェンス命令は、その前の全ロード/ストア命令がフェンス後のロード/ストア命令の前に完了することを保証する。他のアーキテクチャでは、「acquire」命令と「release」命令でメモリバリアを構成する。これはリード・アフター・ライト操作の可視性に関するもので、読み込む側(acquire)と書き込む側(release)がそれぞれの観点で命令を使用する。いくつかのアーキテクチャでは主記憶とI/Oメモリの操作の組合せに合わせて何種類かのメモリバリア命令を提供する。メモリバリア命令が複数種類存在するアーキテクチャでは、それぞれの命令にかかるコストに大きな違いがある点が重要である。

「スレッド化」プログラミングとメモリ可視性

スレッド化されたプログラムは一般に、Javaなどの高級言語が提供する同期プリミティブやPOSIXスレッドやWin32などのAPIが提供する同期プリミティブを使用する。ミューテックスやセマフォなどのプリミティブは、複数スレッドが資源にアクセスするための同期機能を提供する。これらのプリミティブは必要なメモリ可視性(visibility)を提供するためにメモリバリア機能も実装していることが多い。そのような環境ではメモリバリアをプログラマが明示的に使用する必要はない。
各APIやプログラミング環境は、原則として自身の高レベルメモリモデルを持っていて、メモリ可視性を定義している。そのような環境ではメモリバリアを必要とすることはないが、メモリ可視性がどうなっているかを可能な限り理解しておくことは重要である。ただし、そのような話が必ずしも文書化されたり明確化されているわけではない。
プログラミング言語の意味論が機械語の命令コードとは異なったレベルで抽象化されて定義されているように、プログラミング環境のメモリモデルはハードウェアのメモリモデルとは抽象化のレベルが異なっている。これらを区別して理解し、低レベルなメモリバリア命令と特定のプログラミング環境のメモリ可視性の意味が異なることを理解することが重要である。例えば、あるプラットフォームの Pthreads の実装では、要求されているよりも強いメモリバリアを使用しているかもしれない。実装されたメモリ可視性を前提としてプログラムを作成すると、仕様上のメモリ可視性を前提としたプログラムよりも移植性が低くなる可能性がある。

アウトオブオーダー実行とコンパイラによる命令順序の最適化

メモリバリア命令はハードウェアレベルの命令並べ替えに対するものである。コンパイラも最適化処理の一環として命令の並べ替えをすることがある。いずれの場合も並列処理では同様の問題を引き起こすので、マルチスレッドで共有されるデータに関わるコードの最適化を抑制する手段を提供する必要がある。なお、そのような手段が必要となるのは、同期プリミティブで保護されていないデータのみである。
C言語のキーワードvolatileを使用すると、その変数に関するメモリ操作の並べ替えや省略といった最適化を抑制することができる。これは、シングルプロセッサシステムでのマルチスレッドやシグナルハンドラといった割り込みに対する一種のバリアを提供するものである。しかし、volatileはコンパイラの最適化に関するものであるため、マルチプロセッサシステムでの並べ替え問題には不十分である。
いくつかの言語はコンパイラの最適化にもアウト・オブ・オーダー実行にも対処する機能を持つものもあるが、そのコンパイラが生成したコードが本当に並べ替えを行っていないか調べるなど、十分注意して使用すべきである。開発者によっては、コンパイラによる並べ替え問題を防ぐ必要があるときにはアセンブリ言語を使用する者もいる。
Java 1.5（バージョン5とも言う）は新たなメモリモデルを採用しており、キーワードvolatileである種のハードウェアによる命令並べ替えとコンパイラによる並べ替えを防ぐことを保証している。C++でも同様の拡張が提案されている。