「強いメモリモデル」と「弱いメモリモデル」

Apple M1についての面白い記事を見かけて、久しぶりにメモリモデル屋(?)の血が騒いだのでブログを書く。

note.com

強いメモリモデル

現代のCPUアーキテクチャでは、x86(64bit, 32bitどちらも)が「強いメモリモデル」を採用しており、それ以外のメジャーなCPUが「弱いメモリモデル」を採用している。この「強いメモリモデル」「弱いメモリモデル」について、まずおさらいしておこう。

以下のように、2つの変数a, bに対して異なるCPUコアが同時にアクセスしたとする。

int a = 0;
int b = 0;

CPU1:
  a = 1;
  b = 1;

CPU2:
  int r1 = b;
  int r2 = a;

(上記はC言語に似た疑似コードを用いているが、実際は機械語命令になっていると考えてほしい。つまり、CPU1は変数a, bの示すメモリアドレスに対するストア命令を実行しており、CPU2は同じメモリアドレスに対するロード命令を実行しているということである。)

このようなコードが実行されたとき、 r1 == 1 && r2 == 0 という結果は起こり得るだろうか? 「強いメモリモデル」を採用するx86では、そのような結果が起こり得ないことが保証されている。 r1 == 1 は変数bに対してCPU1が書き込みをした後にCPU2が読み込みをしたことを示している。よってその場合、CPU2が変数aから読みだした値 r2 も、CPU1が書き込んだ 1 でなければおかしい、ということである。

しかしこの「強いメモリモデル」はCPUの内部動作に重大な制約を強いることになる。近年のCPUでは当たり前のようにアウト・オブ・オーダー実行が行われているが、先程のコードで素朴に変数a, bへのアクセスをリオーダーしてしまうと、もはや「r1 == 1 && r2 == 0 という結果が起こり得ない」ということを保証できなくなってしまう。そのため、x86アーキテクチャでは以下のようなルールを守った範囲でだけアウト・オブ・オーダー実行などの最適化を行っている。

  • あるCPUが複数のメモリアドレスに対して行った書き込みの結果が「他のCPUから見えるようになる(globally visible)」順序は、機械語命令の順序と矛盾してはならない。
    • つまり上の例で言うと、CPU1が変数bへ書き込んだ値は変数aよりも先にCPU2から見えるようになってはならない、ということである。
  • あるCPUが複数のメモリアドレスに対して行った読み込みの結果は、それらがglobally visibleになった順序と矛盾してはならない。
    • つまり上の例で言うと、CPU2がある時点の変数bの値を読みだした場合、その時点での変数aの値(=その時点でglobally visibleになっている値)よりも古い値を読み出してはならない、ということである。
    • 変数aのメモリアドレスだけがキャッシュメモリに乗っている場合などは、変数bの読み込みよりも先に変数aの読み込みを投機的に実行するのは認められている。しかしその場合でも、上記のルールに矛盾する可能性を検知したときは投機的実行をキャンセルして変数aの読み込みをやり直さなければならない。

かなり複雑ではあるが、このルールをほぼ全てのメモリアクセスに対して適用することでx86アーキテクチャは「強いメモリモデル」を実現しているのである。

弱いメモリモデル

さて、ここまでの解説を読んだら当然浮かぶ疑問は「もしこの強いメモリモデルのルールを捨てることができれば、CPUはより高速に動作できるようになるのではないか」というものであろう。

実際、強いメモリモデルはほとんどのプログラムコードでは不要である。メモリモデルとは上で述べたように「同じメモリアドレスに対して複数のCPUコアが同時にアクセスした」場合の挙動を定義するものであるので、mutexなどによって排他制御を正しく行っていれば考える必要のないものである。しかし一方で、このmutexのようなマルチスレッドライブラリ自身の内部実装では、メモリモデルに関する考慮が非常に重要になってくる。

そのため、デフォルトでは「強いメモリモデル」を提供せずにもっと緩い条件を認める一方、一部のマルチスレッドプログラム用にメモリアクセスの順序を明示的に指定するための機械語命令を用意したアーキテクチャ、というものを考えることができる。これがx86以外の現代の多くのCPUアーキテクチャで用いられている「弱いメモリモデル」である。

「弱いメモリモデル」は「強いメモリモデル」よりも制約が緩い分、CPU内部実装の最適化・高速化の面で有利なはずである。しかし何故x86は現在もまだ強いメモリモデルを採用しているのかというと、互換性の問題があるからである。デフォルトのメモリモデルを変えてしまうと、強いメモリモデルを前提として実装されている既存のマルチスレッドライブラリが誤動作してしまうのである。そのため、デフォルトのメモリモデルを変更することは、命令セットの切り替えのような大幅な刷新が行われるタイミングでないと難しい。

インテルもItanium(IA-64)プロセッサでは弱いメモリモデルを採用したのだが、御存知の通りItaniumは終了してしまっている。対抗のx86-64(AMD64)アーキテクチャは既存の32bitコードとの互換性を重視したために以前と同じ強いメモリモデルを採用したので、x86アーキテクチャはメモリモデルの切り替えを行う最大のチャンスを逃してしまったということになる。

だいぶ長くなってしまったので、続きは別記事で。