不正なコンパイル
Go メモリモデルは、Go プログラムと同様にコンパイラーの最適化を制限します。シングルスレッドプログラムでは有効であるいくつかのコンパイラーの最適化は、すべての Go プログラムで有効ではありません。特に、コンパイラーは元のプログラムに存在しない書き込みを導入してはならず、単一の読み取りが複数の値を観測することを許可してはならず、単一の書き込みが複数の値を書き込むことを許可してはなりません。
以下のすべての例は、*p
と *q
が複数のゴルーチンにアクセス可能なメモリ位置を参照していることを想定しています。
レースフリープログラムにデータレースを導入しないことは、それらが現れる条件文から書き込みを移動しないことを意味します。例えば、コンパイラーはこのプログラムの条件を反転してはなりません:
*p = 1
if cond {
*p = 2
}
つまり、コンパイラーはプログラムを次のように書き換えてはなりません:
*p = 2
if !cond {
*p = 1
}
cond
が false で別のゴルーチンが *p
を読み取っている場合、元のプログラムでは、他のゴルーチンは *p
の以前の値と 1
のみを観測できます。書き換えられたプログラムでは、他のゴルーチンは 2
を観測できますが、これは以前は不可能でした。
データレースを導入しないことは、ループが終了することを想定しないことも意味します。例えば、コンパイラーは通常、このプログラムのループの前に *p
や *q
へのアクセスを移動してはなりません:
n := 0
for e := list; e != nil; e = e.next {
n++
}
i := *p
*q = 1
list
が循環リストを指している場合、元のプログラムは *p
や *q
にアクセスすることはありませんが、書き換えられたプログラムはアクセスします。(*p
を前に移動することは、コンパイラーが *p
がパニックしないことを証明できる場合は安全です;*q
を前に移動することは、コンパイラーが他のゴルーチンが *q
にアクセスできないことを証明することも必要です。)
データレースを導入しないことは、呼び出される関数が常に戻ることや、同期操作がないことを想定しないことも意味します。例えば、コンパイラーは(少なくとも f
の正確な動作を直接知らない限り)このプログラムの関数呼び出しの前に *p
や *q
へのアクセスを移動してはなりません:
f()
i := *p
*q = 1
呼び出しが戻らない場合、元のプログラムは *p
や *q
にアクセスすることはありませんが、書き換えられたプログラムはアクセスします。また、呼び出しに同期操作が含まれている場合、元のプログラムは *p
や *q
へのアクセスに先行するhappens-before エッジを確立することができますが、書き換えられたプログラムはそうではありません。
単一の読み取りが複数の値を観測することを許可しないことは、共有メモリからローカル変数を再読み込みしないことを意味します。例えば、コンパイラーはこのプログラムで i
を破棄して *p
から2回目に再読み込みしてはなりません:
i := *p
if i < 0 || i >= len(funcs) {
panic("invalid function index")
}
... complex code ...
// compiler must NOT reload i = *p here
funcs[i]()
複雑なコードが多くのレジスターを必要とする場合、シングルスレッドプログラム用のコンパイラーは i
のコピーを保存せずに破棄し、funcs[i]()
の直前に i = *p
を再読み込みすることができます。Go コンパイラーはそうしてはなりません。*p
の値が変更されている可能性があるからです。(代わりに、コンパイラーは i
をスタックにスピルできます。)
単一の書き込みが複数の値を書き込むことを許可しないことは、書き込み前にローカル変数が書き込まれるメモリを一時的な記憶域として使用しないことも意味します。例えば、コンパイラーはこのプログラムで *p
を一時的な記憶域として使用してはなりません:
*p = i + *p/2
つまり、プログラムを次のように書き換えてはなりません:
*p /= 2
*p += i
i
と *p
が2で始まる場合、元のコードは *p = 3
を行うため、競合するスレッドは *p
から2または3のみを読み取ることができます。書き換えられたコードは *p = 1
を行い、その後 *p = 3
を行うため、競合するスレッドは1も読み取ることができます。
これらすべての最適化は C/C++ コンパイラーで許可されていることに注意してください:C/C++ コンパイラーとバックエンドを共有する Go コンパイラーは、Go では無効な最適化を無効にするよう注意する必要があります。
データレースを導入することの禁止は、コンパイラーがレースがターゲットプラットフォームでの正しい実行に影響しないことを証明できる場合は適用されないことに注意してください。例えば、本質的にすべての CPU で、以下のように書き換えることは有効です:
n := 0
for i := 0; i < m; i++ {
n += *shared
}
次のように:
n := 0
local := *shared
for i := 0; i < m; i++ {
n += local
}
*shared
がアクセス時に障害を起こさないことが証明できる場合、追加される読み取りは既存の並行読み取りや書き込みに影響しないためです。一方、この書き換えは source-to-source トランスレーターでは有効ではありません。