Full fences
私は、そのフェンスの周りで命令の並べ替えやキャッシュを一切行わないと読んだことがあります(memoryBarrier経由)。
volatile
次に、 「ハーフフェンス」を生成するものについて読みました。
volatile キーワードは、そのフィールドからの読み取りごとに acquire-fence を生成し、そのフィールドへの書き込みごとに release-fence を生成するようにコンパイラに指示します。
acquire-fence
取得フェンスは、他の読み取り/書き込みがフェンスの前に移動されるのを防ぎます。
release-fence
リリース フェンスは、フェンスの後に他の読み取り/書き込みが移動されるのを防ぎます。
誰かこの2つの文を簡単な英語で説明してくれませんか?
(フェンスはどこですか?)
編集
ここでいくつかの回答があった後、私は皆の役に立つ絵を描いたと思います。
ベストアンサー1
あなたが言及している文言は、私がよく使用するもののようです。ただし、仕様には次のように書かれています。
- 揮発性フィールドの読み取りは、揮発性読み取りと呼ばれます。揮発性読み取りには「取得セマンティクス」があります。つまり、命令シーケンス内でその後に発生するメモリへの参照よりも前に発生することが保証されます。
- 揮発性フィールドの書き込みは、揮発性書き込みと呼ばれます。揮発性書き込みには「リリース セマンティクス」があります。つまり、命令シーケンス内の書き込み命令の前のメモリ参照の後に必ず発生するということです。
しかし、私は通常、あなたの質問で引用した言葉を使用します。なぜなら、指示が移動できるあなたが引用した文言と仕様は同等です。
いくつかの例を示します。これらの例では、リリース フェンスを示すために ↑ 矢印を使用し、取得フェンスを示すために ↓ 矢印を使用する特別な表記を使用します。他の命令は、↑ 矢印を越えて下向きに、または ↓ 矢印を越えて上向きにフロートすることはできません。矢印の先端がすべてをはじき飛ばすと考えてください。
次のコードを考えてみましょう。
static int x = 0;
static int y = 0;
static void Main()
{
x++
y++;
}
個々の指示を示すように書き直すと、次のようになります。
static void Main()
{
read x into register1
increment register1
write register1 into x
read y into register1
increment register1
write register1 into y
}
この例ではメモリバリアがないので、C#コンパイラ、JITコンパイラ、ハードウェアはさまざまな方法で自由に最適化できます。実行中のスレッドによって認識される論理シーケンスが物理シーケンスと一致している限りx
ここにそのような最適化の 1 つを示します。との読み取りと書き込みがy
スワップされていることに注意してください。
static void Main()
{
read y into register1
read x into register2
increment register1
increment register2
write register1 into y
write register2 into x
}
今回は、これらの変数を に変更しますvolatile
。矢印表記を使用してメモリバリアをマークします。 と の読み取りと書き込みの順序がどのように保持されるかに注目してくださいx
。y
これは、命令がバリア (↓ と ↑ の矢印で示される) を通過できないためです。これは重要です。x
命令の増分と書き込みは引き続きフロートダウンでき、 の読み取りはy
フロートアップできることに注目してください。これは、ハーフフェンスを使用しているため、依然として有効です。
static volatile int x = 0;
static volatile int y = 0;
static void Main()
{
read x into register1
↓ // volatile read
read y into register2
↓ // volatile read
increment register1
increment register2
↑ // volatile write
write register1 into x
↑ // volatile write
write register2 into y
}
これは非常に些細な例です。私の答えを見てくださいここvolatile
ダブルチェック パターンでどのように違いが生じるかを示す簡単な例です。ここで使用したのと同じ矢印表記を使用して、何が起こっているかを簡単に視覚化します。
これで、Thread.MemoryBarrier
操作するメソッドもできました。完全なフェンスを生成します。矢印表記を使用すれば、その動作も視覚化できます。
この例を考えてみましょう。
static int x = 0;
static int y = 0;
static void Main
{
x++;
Thread.MemoryBarrier();
y++;
}
個々の命令を前と同じように表示すると、次のようになります。命令の移動が完全に阻止されていることに注意してください。命令の論理的な順序を損なわずにこれを実行する方法は他にありません。
static void Main()
{
read x into register1
increment register1
write register1 into x
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
read y into register1
increment register1
write register1 into y
}
さて、もう1つ例を挙げましょう。今回はVB.NETを使用します。VB.NETにはキーワードがありませんvolatile
。では、VB.NETで揮発性読み取りを模倣するにはどうすればよいでしょうか。 を使用しますThread.MemoryBarrier
。1
Public Function VolatileRead(ByRef address as Integer) as Integer
Dim local = address
Thread.MemoryBarrier()
Return local
End Function
矢印表記ではこのようになります。
Public Function VolatileRead(ByRef address as Integer) as Integer
read address into register1
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
return register1
End Function
Thread.MemoryBarrier
揮発性の読み取りを模倣したいので、呼び出しは次のように配置する必要があることに注意することが重要です。後実際の読み取り。揮発性の読み取りは「新しい読み取り」を意味し、揮発性の書き込みは「コミットされた書き込み」を意味すると考える罠に陥らないでください。それは動作方法ではなく、仕様で説明されていることでもありません。
アップデート:
画像を参考に。
待ってください!すべての書き込みが完了したことを確認しています。
そして
待ってください!すべてのコンシューマーが現在の値を取得していることを確認しています。
これが私が言っていた罠です。記述は完全に正確ではありません。確かに、ハードウェアレベルで実装されたメモリバリアはキャッシュコヒーレンシラインを同期させる可能性があり、その結果、上記の記述はある程度正確な説明となるかもしれません。しかし、volatile
命令の移動を制限する以上のことは何もしません。仕様では、何もないメモリバリアが配置されている場所でメモリから値をロードするか、メモリに保存するかについて説明します。
1もちろん、Thread.VolatileRead
すでに組み込み関数は存在します。そして、ここで私が行ったのとまったく同じように実装されていることに気づくでしょう。