メモリバリアメモリバリア(英: 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" が表示される可能性も出てくる。上記の例のように f がプロセッサ間の同期を担っている場合や、f へのストアがメモリマップドI/Oを介したデバイスへのコマンド発行(この場合、プロセッサ#1 がデバイスに置き換わる)であるケースではこのような挙動は受け入れられない。メモリバリアをプロセッサ#2の位置 f へのストア命令の直前に挿入すると、他のプロセッサから見ても x が f の前に更新されているように観測されることを保証できる。 低レベルアーキテクチャ向けのプリミティブメモリバリアは、アーキテクチャのメモリモデルの定義の一部の低レベルプリミティブである。命令セットと同様、メモリモデルはアーキテクチャによって様々であるため、その動作を概括的に述べることは適切ではない。一般にメモリバリアを正しく使用するには、プログラミング対象のハードウェアのアーキテクチャマニュアルを読むべきである。とはいうものの、以下ではいくつかの実在するメモリバリアについて紹介する。 いくつかのアーキテクチャでは、「フルフェンス」と呼ばれる一種類のメモリバリア命令だけを提供する。フルフェンス命令は、その前の全ロード/ストア命令がフェンス後のロード/ストア命令の前に完了することを保証する。一例としては PowerPC の 順序性が問題になるメモリの内容が単一の整数値など1つのメモリ操作命令のみで操作できる場合、これとメモリバリアを組み合わせることによりロックプリミティブよりもさらに軽量な同期機構を実装することができる。参照カウントやメモリ上のフラグが典型例である他、スピンロックやミューテックスのような軽量ロックプリミティブにあってもロック変数の実装手段としてしばしば使用する。環境によってはメモリ操作命令とメモリバリアの組み合わせの需要が多いことから、これらをAPIとして整備していることがある。例えばLinuxカーネルではマクロとして マルチスレッドプログラミングとメモリ可視性マルチスレッド化されたプログラムは一般に、POSIXスレッド(Pthreads)あるいはWin32などのAPIが提供する同期プリミティブや、(通例そのようなAPIを使って実装された)Javaや.NETなどの高水準言語あるいはフレームワークが提供する同期プリミティブまたは同期構文を使用する。ミューテックスやセマフォなどのプリミティブは、複数スレッドが共有資源にアクセスするための同期機能を提供する[注釈 1]。これらのプリミティブは必要なメモリ可視性 (visibility) を提供するためにメモリバリア機能も実装していることが多い[6][7]。そのような保証のある環境においてこれらの同期機構による排他制御を実装する場合は、メモリバリアをプログラマが明示的に使用する必要はない。ほとんどの用途では、同期プリミティブや同期構文によって提供される暗黙的なメモリバリアのほうが簡潔な手段である[8]。 各APIやプログラミング環境は、原則として自身の高レベルメモリモデルを持っていて、メモリ可視性を定義している。そのような環境ではメモリバリアを必要とすることはないが、メモリ可視性がどうなっているかを可能な限り理解しておくことは重要である。ただし、メモリモデルの仕様が必ずしも文書化されていたり明確化されていたりするわけではない。 プログラミング言語のセマンティクス(意味論)が機械語の命令コードとは異なったレベルで抽象化されて定義されているように、プログラミング環境のメモリモデルはハードウェアのメモリモデルとは抽象化のレベルが異なっている。これらを区別して理解し、低レベルなメモリバリア命令と特定のプログラミング環境のメモリ可視性の意味が異なることを理解することが重要である。例えば、あるプラットフォームにおけるPthreadsの実装では、POSIX規格で要求されているものよりも強いメモリバリアを使用しているかもしれない。実装されたメモリ可視性を前提としてプログラムを作成すると、仕様上のメモリ可視性を前提としたプログラムよりも移植性が低くなる可能性がある。 アウトオブオーダー実行とコンパイラによる命令順序の最適化メモリバリア命令はハードウェアレベルの命令並べ替えに対するものである[9]。メモリマップドI/Oアクセス時に必要とされる。コンパイラも最適化処理の一環として命令の並べ替えをすることがある。いずれの場合も並列処理では同様の問題を引き起こすので、マルチスレッドで共有されるデータに関わるコードの最適化を抑制する手段を提供する必要がある。なお、そのような手段が必要となるのは、同期プリミティブで保護されていないデータのみである。 C言語およびC++のキーワード いくつかの言語はコンパイラの最適化にもアウト・オブ・オーダー実行にも対処する機能を持つものもあるが、そのコンパイラが生成したコードが本当に並べ替えを行っていないか調べるなど、十分注意して使用すべきである。コンパイラによる並べ替え問題を防ぐ必要があるときにはアセンブリ言語を直接使用することも考えられるが、一般的ではなく、生産性やメンテナンス性の観点からは現実的な選択肢ではない。コンパイラやオペレーティングシステムが用意しているアトミック操作のための組み込み関数やAPI関数を利用するなどして、リオーダーを抑制すべきである。 Java 1.5(Java 5とも呼ばれる)は新たなメモリモデルを採用しており、キーワード 関連項目脚注注釈出典
外部リンクいずれも英文
|