メモリモデル緩和アノテーション

はじめに

メモリモデル緩和アノテーション (MMRA) は、命令に対するターゲット定義のプロパティであり、メモリモデルによって課せられる制約を選択的に緩和するために使用できます。例えば

  • SPIRVプログラムでVulkanMemoryModelを使用すると、特定のメモリオペレーションをacquireまたはrelease操作を跨いで並べ替えることができます。

  • OpenCL APIは、特定のアドレス空間のセットのみをフェンスするプリミティブを公開しています。その情報をバックエンドに伝えることで、毎回すべてのアドレス空間をフェンスするのではなく、より高速な同期命令を使用できるようになります。

MMRAは、ターゲットがデフォルトのLLVMメモリモデルを緩和するためのオプトインシステムを提供します。そのため、LLVMメタデータを使用して操作に添付され、常に正しさに影響を与えることなく削除できます。

定義

メモリオペレーション

メモリにアクセスするものとしてマークされたロード、ストア、アトミック、または関数呼び出し。

同期操作

他のスレッドとメモリを同期させる命令 (例: アトミックまたはフェンス)。

タグ

メモリまたは同期操作に添付されたメタデータで、メモリ同期に関するターゲット定義のプロパティを表します。

操作には、それぞれ異なるプロパティを表す複数のタグが付いている場合があります。

タグは、メタデータ文字列のペア ( *プレフィックス* と *サフィックス* ) で構成されます。

LLVM IRでは、ペアはメタデータタプルを使用して表されます。他の場合 (コメント、ドキュメントなど) では、prefix:suffix表記を使用する場合があります。例えば

リスト 20 例: メタデータのタグ
!0 = !{!"scope", !"workgroup"}  # scope:workgroup
!1 = !{!"scope", !"device"}     # scope:device
!2 = !{!"scope", !"system"}     # scope:system

注記

オプティマイザに関連する唯一のセマンティクスは、以下で定義されている「互換性」関係です。他のすべてのセマンティクスはターゲット定義です。

タグはリストに編成して、操作が属するすべてのタグを指定できるようにすることもできます。このようなリストは「タグのセット」と呼ばれます。

リスト 21 例: メタデータのタグのセット
!0 = !{!"scope", !"workgroup"}
!1 = !{!"sync-as", !"private"}
!2 = !{!0, !2}

注記

操作にMMRAメタデータがない場合、タグの空のリスト (!{}) があるかのように扱われます。

タグが適用されている命令、または現在のターゲットによって認識されない場合でも、エラーではありません。そのようなタグは単に無視されます。

同期操作とメモリオペレーションの両方には、!mmra構文を使用してゼロ個以上のタグを添付できます。

以下の例を読みやすくするために、MMMRAメタデータを表す (機能しない) 短縮構文を使用します

リスト 22 短縮構文の例
store %ptr1 # foo:bar
store %ptr1 !mmra !{!"foo", !"bar"}

これらの2つの表記法はこのドキュメントで使用でき、厳密に等価です。ただし、機能するのは2番目のバージョンのみです。

互換性

少なくとも1つのセットに存在する一意のタグプレフィックスPごとに、2つのタグのセットは *互換性がある* と言われます

  • もう一方のセットにはプレフィックスPのタグが含まれていないか、

  • プレフィックスPのタグが少なくとも1つ両方のセットに共通している。

上記の定義は、空のセットは常に他のセットと互換性があることを意味します。これは、変換が操作のメタデータを削除した場合、正しさに影響を与えることがないため、重要なプロパティです。言い換えれば、命令からメタデータを削除することでメモリモデルをさらに緩和することはできません。

*happens-before* 関係

互換性チェックを使用して、2つの命令の間に確立された *happens-before* 関係からオプトアウトできます。

順序付け

2つの命令のメタデータに互換性がない場合、それらの間のプログラム順序は *happens-before* ではありません。

たとえば、ターゲットによって公開される2つのタグfoo:barfoo:bazを考えてみましょう

A: store %ptr1                 # foo:bar
B: store %ptr2                 # foo:baz
X: store atomic release %ptr3  # foo:bar

上の図では、AXと互換性があるため、A happens-before Xです。しかし、BXと互換性がないため、happens-before Xではありません。

同期

同期操作に1つ以上のタグがある場合、それが他の操作と同期し、seq_cst順序に参加するかどうかはターゲットに依存します。

次の例が別のシーケンスと同期するかどうかは、foo:barfoo:buxのターゲット定義のセマンティクスに依存します。

fence release               # foo:bar
store atomic %ptr1          # foo:bux

例1
A: store ptr addrspace(1) %ptr2                  # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3   # sync-as:0 vulkan:nonprivate

AとBは、タグのセットに互換性がないため、互いに順序付けられていません ( *happens-before* なし)。

sync-as値はaddrspace値と一致する必要はありません。例: 例1では、addrspace(1)の場所へのストアリリースは、addrspace(0)で発生する操作とのみ同期したいと考えています。

例2
A: store ptr addrspace(1) %ptr2                 # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3  # sync-as:1 vulkan:nonprivate

AとBの順序は、タグのセットに互換性があるため、影響を受けません。

AとBは、他の理由により *happens-before* にある場合とない場合があります。

例3
A: store ptr addrspace(1) %ptr2                 # sync-as:1 vulkan:nonprivate
B: store atomic release ptr addrspace(1) %ptr3  # vulkan:nonprivate

AとBの順序は、タグのセットに互換性があるため、影響を受けません。

例4
A: store ptr addrspace(1) %ptr2                 # sync-as:1
B: store atomic release ptr addrspace(1) %ptr3  # sync-as:2

AとBは、タグのセットに互換性がないため、互いに順序付けられている必要はありません ( *happens-before* なし)。

ユースケース

SPIRV NonPrivatePointer

MMRAは、同期操作がNonPrivatePointerセマンティクスを指定するメモリオペレーションにのみ影響を与えるSPIRV機能VulkanMemoryModelをサポートできます。

以下の例は、次のレシピを使用してSPIRVプログラムから生成されます

  • すべての同期操作にvulkan:nonprivateを追加します。

  • NonPrivatePointerとマークされているすべての非アトミックメモリオペレーションにvulkan:nonprivateを追加します。

  • NonPrivatePointerとマークされていないすべての非アトミックメモリオペレーションのタグにvulkan:privateを追加します。

Thread T1:
 A: store %ptr1                 # vulkan:nonprivate
 B: store %ptr2                 # vulkan:private
 X: store atomic release %ptr3  # vulkan:nonprivate

Thread T2:
 Y: load atomic acquire %ptr3   # vulkan:nonprivate
 C: load %ptr2                  # vulkan:private
 D: load %ptr1                  # vulkan:nonprivate

互換性により、操作AXに対して順序付けられ、操作DYに対して順序付けられます。XYと同期する場合、A happens-before Dです。操作BCについては、そのような関係を推測することはできません。

注記

Vulkanメモリモデルは、すべてのアトミック操作を非プライベートと見なします。

アトミック操作は常にnonprivateであるため、vulkan:nonprivateがアトミック操作で指定されるかどうかは実装の詳細です。実装は明示的で、すべてのアトミック操作にvulkan:nonprivateを含むIRを発行することを選択するか、vulkan::privateのみを発行し、デフォルトでvulkan:nonprivateを想定することを選択できます。

vulkan:private とマークされた操作は、SPIRV プログラムにおける happens-before 順序から事実上オプトアウトします。これは、すべての同期操作と互換性がないためです。NonPrivatePointer とマークされていない SPIRV 操作は、スレッドに対して完全にプライベートではありません。Vulkan の *system-synchronizes-with* 関係によって、スレッドの開始時または終了時に暗黙的に同期されます。この例では、vulkan:private のターゲット定義のセマンティクスがこのプロパティを正しく実装していると仮定しています。

このスキームは、SPIRV プログラムと他の環境との相互運用性を表現するのに十分な汎用性を備えています。

Thread T1:
A: store %ptr1                 # vulkan:nonprivate
X: store atomic release %ptr2  # vulkan:nonprivate

Thread T2:
Y: load atomic acquire %ptr2   # foo:bar
B: load %ptr1

上記の例では、スレッド T1 は SPIRV プログラムから発生し、スレッド T2 は非 SPIRV プログラムから発生します。XY と同期できるかどうかは、ターゲットによって定義されます。XY と同期する場合、AB より前に発生します(A/X と Y/B は互換性があるため)。

実装例

すべてのメモリ操作がキャッシュされ、キャッシュ全体が release または acquire でそれぞれフラッシュまたは無効化されるターゲットにおける、SPIRV NonPrivatePointer の実装を考えてみましょう。可能なスキームとしては、SPIRV プログラムを翻訳する際に、NonPrivatePointer とマークされたメモリ操作はキャッシュせず、acquire および release 操作中はキャッシュの内容に触れないようにすることです。

これは、vulkan: 接頭辞を共有するタグを使用して、次のように実装できます。

  • メモリ操作の場合

    • vulkan:nonprivate を使用した操作は、キャッシュをバイパスする必要があります。

    • vulkan:private を使用した操作は、キャッシュする必要があります。

    • どちらも指定しない、または両方を指定する操作は、正確性を確保するために、保守的にキャッシュをバイパスする必要があります。

  • 同期操作の場合

    • vulkan:nonprivate を使用した操作は、キャッシュをフラッシュまたは無効化してはなりません。

    • vulkan:private を使用した操作は、キャッシュをフラッシュまたは無効化する必要があります。

    • どちらも指定しない、または両方を指定する操作は、正確性を確保するために、保守的にキャッシュをフラッシュまたは無効化する必要があります。

注記

このような実装では、操作のメタデータを削除しても正確性には影響しませんが、パフォーマンスに大きな影響を与える可能性があります。たとえば、操作が不必要にキャッシュをバイパスするなどです。

メモリタイプ

MMRA は、異なるメモリタイプの選択的な同期を表現できます。

たとえば、ターゲットは、同期操作の実行によってどのアドレス空間が同期されるかについての情報を渡すために、sync-as:<N> タグを公開する場合があります。

注記

ここではアドレス空間が一般的な例として使用されていますが、この概念は他の「メモリタイプ」にも適用できます。ここで「メモリタイプ」が何を意味するかは、ターゲット次第です。

# let 1 = global address space
# let 3 = local address space

Thread T1:
A: store %ptr1                                  # sync-as:1
B: store %ptr2                                  # sync-as:3
X: store atomic release ptr addrspace(0) %ptr3  # sync-as:3

Thread T2:
Y: load atomic acquire ptr addrspace(0) %ptr3   # sync-as:3
C: load %ptr2                                   # sync-as:3
D: load %ptr1                                   # sync-as:1

上の図では、XYglobal アドレス空間の位置に対するアトミック操作です。XY と同期する場合、Blocal アドレス空間で C より前に発生します。しかし、操作 AD については、global アドレス空間の位置で実行されているにもかかわらず、そのようなことは言えません。

実装例:アドレス空間情報をフェンスに追加する

OpenCL C などの言語は、フェンスする明示的なアドレス空間を取ることができる atomic_work_item_fence などのフェンス操作を提供します。

デフォルトでは、LLVM は IR でその情報を伝える手段がないため、LLVM IR への Lowering 中に情報が失われます。これは、AMDGPU などのターゲットが、すべての場合においてすべてのアドレス空間をフェンスする命令を保守的に発行する必要があることを意味し、高性能アプリケーションではパフォーマンスに大きな影響を与える可能性があります。

MMRA は、コード生成を通して、IR レベルでその情報を保持するために使用できます。たとえば、グローバルアドレス空間 addrspace(1) のみに影響を与えるフェンスは、次のように Lowering できます。

fence release # sync-as:1

ターゲットは sync-as:1 の存在を使用して、グローバルアドレス空間をフェンスする命令のみを発行する必要があると推論できます。

MMRA はオプトインであるため、MMRA メタデータを持たないフェンスは引き続き保守的に Lowering できることに注意してください。そのため、この最適化は、フロントエンドがフェンス命令に MMRA メタデータを発行する場合にのみ適用されます。

追加トピック

注記

以下のセクションは参考情報です。

パフォーマンスへの影響

MMRA は、プログラムにおける最適化の機会を捉える方法です。しかし、操作がタグに言及していない場合、または競合するタグがある場合、ターゲットはパフォーマンスを犠牲にして正確性を確保するために保守的なコードを生成する必要がある場合があります。これは、次のような状況で発生する可能性があります。

  1. ターゲットが最初に MMRA を導入したとき、フロントエンドがそれらを発行するように更新されていない可能性があります。

  2. 最適化によって MMRA メタデータが削除される可能性があります。

  3. 最適化によって操作に任意のタグが追加される可能性があります。

ターゲットは、常に MMRA を無視する(または削除する)ことを選択し、正確性に影響を与えることなく、デフォルトの動作/コード生成ヒューリスティックに戻すことができることに注意してください。

*happens-before* が存在しない場合の影響

happens-before セクションでは、MMRA 間の互換性を利用して、2 つの命令間の *happens-before* 関係をどのように破ることができるかを定義しました。命令に互換性がなく、*happens-before* 関係がない場合、命令は「互いに順序付けられる必要がない」と言います。

このコンテキストにおける「順序付け」は、静的およびランタイムの両方の側面を網羅する非常に広い用語です。

順序付けの制約がない場合、再順序付けが単一ロケーションのコヒーレンスなどの他の制約に違反しない限り、オプティマイザ変換で命令を静的に再順序付け*できます*。静的再順序付けは *happens-before* を破ることの 1 つの結果ですが、最も興味深いものではありません。

ランタイムの結果はより興味深いです。命令間に *happens-before* 関係がある場合、ターゲットは同期コードを発行して、他のスレッドが正しい順序で命令の効果を観測できるようにする必要があります。

たとえば、ターゲットはフェンスリリースを開始する前に以前のロードとストアが完了するのを待つ必要がある場合や、次の命令を実行する前にメモリキャッシュをフラッシュする必要がある場合があります。*happens-before* がない場合、そのような要件はなく、待機やフラッシュは必要ありません。これは、場合によっては実行速度を著しく向上させる可能性があります。

操作の組み合わせ

パスが複数のメモリまたは同期操作を 1 つに組み合わせることができる場合、MMRA を組み合わせることができる必要があります。これを達成する 1 つの可能な方法は、タグセットのプレフィックス単位の和集合を実行することです。

A と B を 2 つのタグセット、U を A と B のプレフィックス単位の和集合とします。A または B に存在する一意のタグプレフィックス P ごとに

  • A または B のいずれかにプレフィックス P を持つタグがない場合、プレフィックス P を持つタグは U に追加されません。

  • A と B の両方にプレフィックス P を持つタグが少なくとも 1 つある場合、両方のセットからプレフィックス P を持つすべてのタグが U に追加されます。

パスは、MMRA を積極的に組み合わせることを避ける必要があります。これは、情報の大きな損失につながる可能性があるためです。これは正確性に影響を与えることはありませんが、パフォーマンスに影響を与える可能性があります。

一般的な経験則として、操作を積極的に組み合わせたり並べ替えたりする SimplifyCFG などの一般的なパスは、同一のタグセットを持つ命令のみを組み合わせる必要があります。それほど頻繁に組み合わせないパス、または MMRA を組み合わせるコストをよく知っているパスは、上記で説明したプレフィックス単位の和集合を使用できます。

A: store release %ptr1  # foo:x, foo:y, bar:x
B: store release %ptr2  # foo:x, bar:y

# Unique prefixes P = [foo, bar]
# "foo:x" is common to A and B so it's added to U.
# "bar:x" != "bar:y" so it's not added to U.
U: store release %ptr3  # foo:x
A: store release %ptr1  # foo:x, foo:y
B: store release %ptr2  # foo:x, bux:y

# Unique prefixes P = [foo, bux]
# "foo:x" is common to A and B so it's added to U.
# No tags have the prefix "bux" in A.
U: store release %ptr3  # foo:x
A: store release %ptr1
B: store release %ptr2  # foo:x, bar:y

# Unique prefixes P = [foo, bar]
# No tags with "foo" or "bar" in A, so no tags added.
U: store release %ptr3