前回、FENCE命令における命令の順序について紹介したが、メモリアクセスにおいてより厳密な定義がある:
- Acquire アノテーション:ある命令に対してこのアノテーションが付属していると、この命令以降がクリティカルセクションであることを意味し、この命令よりも後ろの命令は、この命令を超えて速く実行を開始することはできないことを意味する(厳密には、速く実行を開始することが観測できてはならない)。
- Release アノテーション:ある命令に対してこのアノテーションが付属していると、この命令以前がクリティカルセクションであることを意味し、この命令よりも前の命令は、この命令を超えて遅く実行を開始することはできないことを意味する(厳密には、速く実行を開始することが観測できてはならない)。
このアノテーションが適用されるのが、RISC-VのAtomicメモリ・アクセス命令である。
orderingのビットに、ac/rlを指定することができる。これは、このアトミック・メモリ・アクセス命令がクリティカルセクションのどちらで実行されるかどうかを示している。
例えば、AMOSWAP.W.AQ
命令はacquireアノテーションが付いているので、このアトミック操作から後続のメモリアクセス命令はAMOSWAPを超えてメモリアクセスを実行してはならないし、AMOSWAP.W.RL
命令はreleaseアノテーションが付いているので、アトミック操作が実行される時には、その前の命令は実行されていなければならない。
以下のサンプルコードについて考えてみる。このコードでは、amoswap.w.aq
により、クリティカルセクションの命令は必ずamoswap.w.aq
よりも後に実行されることが保証されるし、amoswap.w.rl
により、クリティカルセクションの命令は必ずamoswap.w.rl
よりも先に実行されることが保証される。
一方で、このコードだけでは、"Arbitrary unrelated store/load"と示されているクリティカルセクションとは関係ない命令が、クリティカルセクションよりも前に実行されることを保証するものではない。
そこで、amoswapによるクリティカル・セクションに加え、FENCEを挿入することでより厳密にクリティカルセクション内の命令実行を制御する。
amoswap.w
は、aquireアノテーションを除去する代わりに、fence r,rw
を強制し、クリティカルセクションのRead/Write命令が追い越されて実行されることを禁止する。また、クリティカルセクションよりも前のLoad命令が、クリティカルセクションン内にリオーダリングされることを禁止している。
また、amoswap.w.rl
からReleaseアノテーションを除去し、fence rw, w
を実行する。
この効果は、クリティカルセクションのRead/Writeがクリティカルセクション内で完了することを保証し、クリティカルセクションよりも後のWrite操作がクリティカルセクション内にリオーダリングされてしまうことを禁止する。
何となく理解はできるのだが、ではなぜクリティカルセクション前のFENCEはFENCE R, RW
なのだろうか?逆に言えば、なぜPredecessorのRead操作はクリティカルセクションに入り込むことを許さず、Writeは許されるのだろうか。
さらに、クリティカルセクション最後のFENCEはFENCE RW, W
なのだろうか。SuccessorのWriteは禁止され、Readは追い越しを許可されるのはなぜなのか?
良く分からないので、ChatGPTに聞いてみた。合っているかは分からない:
ロードのリオーダリングを禁止する理由:
データ整合性: クリティカルセクションに入る前に、必要な全てのデータが最新の状態であることを保証する必要があります。acquireアノテーションは、ロックを取得する前に実行されるべき全てのロード操作が、実際にロック取得より前に完了することを保証します。これにより、クリティカルセクション内で使用されるデータが他のスレッドによる変更から保護され、最新の状態であることが保証されます。
可視性の保証: クリティカルセクションに入る前に実行されたロード操作が、クリティカルセクション内で利用可能な最新の情報を反映するようにすることで、共有リソースに対する変更が全てのスレッドに正しく可視化されます。
ストアのリオーダリングを許可する理由: パフォーマンス最適化: ストア操作は、他のスレッドがそのデータを読み取る前には通常「見えない」ため、ストアのリオーダリングはデータ整合性に直接的な影響を与えません。したがって、ロックを取得する前のストアのリオーダリングは、プログラムの実行効率を高めるために許可されることがあります。
リソースの衝突回避: ストアのリオーダリングを許可することで、メモリシステム内でのリソースの衝突や競合を減らし、キャッシュやバスの効率を改善することができます。
しかし、重要な点は、acquireアノテーションがクリティカルセクションの正確さと安全性を確保するために、必要なデータのロードの順序を保持することです。同時に、プログラムのパフォーマンスを最適化するために、より柔軟なストアのリオーダリングを許可します。これにより、同期の厳格さとシステムのパフォーマンスのバランスを取ることができます。
ストアのリオーダリングを禁止する理由:
データの可視性と完全性: クリティカルセクション内で行われた変更が、ロックを解放した後に他のスレッドから確実に見えるようにする必要があります。releaseアノテーションにより、クリティカルセクション内の全てのストアがロック解放操作前に行われることが保証されます。これにより、クリティカルセクション内で行われた更新が完全に反映され、データの整合性が維持されます。
後続の操作への安全な公開: クリティカルセクション内で行われた変更は、ロックが解放されることによってのみ、他のスレッドに「公開」されます。releaseアノテーションを使用すると、ロック解放後にのみ他のスレッドがこれらの変更を見ることができるようになり、これにより、クリティカルセクションによる更新が他のスレッドに安全に公開されることが保証されます。
メモリの整合性の保証: ロックが解放された後、他のスレッドがそのロックを取得すると、クリティカルセクション内で行われた全てのメモリ更新が完全に見える状態になっている必要があります。releaseアノテーションがストアのリオーダリングを禁止することで、この整合性が確保されます。
要するに、releaseアノテーションはクリティカルセクション内の変更が他のスレッドに安全に「公開」されることを保証するために、クリティカルセクション後のストア操作の順序を確実に維持します。これは、並行実行されるプログラムにおいて、正しいデータ整合性と同期を保つために重要です。