LLVMにおけるAArch64スケーラブルマトリックス拡張のサポート¶
1. はじめに¶
AArch64 SME ACLE は、PSTATE.SM および PSTATE.ZA を制御するための多数の属性をユーザーに提供します。AArch64 SME ABI は、少なくとも1つの関数が PSTATE.SM または PSTATE.ZA を使用する場合の関数間の呼び出しに関する要件を記述しています。
このドキュメントでは、SME ACLE 属性が LLVM IR 属性にどのようにマップされるか、および LLVM がこれらの属性をどのように低レベルに変換して ABI のルールと要件を実装するかについて説明します。
以下では、LLVM IR 属性と、C/C++レベルの ACLE 属性との関係について説明します。
aarch64_pstate_sm_enabled
__arm_streaming
を持つ関数に使用されます。aarch64_pstate_sm_compatible
__arm_streaming_compatible
を持つ関数に使用されます。aarch64_pstate_sm_body
__arm_locally_streaming
を持つ関数に使用され、関数の定義でのみ有効です(宣言ではありません)。aarch64_new_za
__arm_new("za")
を持つ関数に使用されます。aarch64_in_za
__arm_in("za")
を持つ関数に使用されます。aarch64_out_za
__arm_out("za")
を持つ関数に使用されます。aarch64_inout_za
__arm_inout("za")
を持つ関数に使用されます。aarch64_preserves_za
__arm_preserves("za")
を持つ関数に使用されます。aarch64_expanded_pstate_za
__arm_new_za
を持つ関数に使用されます。
Clang は、上記の属性が関数の宣言/定義と呼び出しサイトの両方に追加されるようにする必要があります。これは、定義または宣言が利用できない、属性付き関数ポインターの呼び出しに重要です。
2. PSTATE.SM の処理¶
PSTATE.SM を変更すると、FP/ベクター演算の実行が別の処理要素に転送される可能性があります。これには3つの重要な意味があります。
ランタイム SVE ベクター長が変わる可能性があります。
FP/AdvSIMD/SVE レジスタの内容がゼロになります。
許可される命令のセットが変わります。
これにより、IR および最適化に特定の制限が生じます。たとえば、PSTATE.SM の値が異なる可能性がある関数間で、ベクター長に依存する状態を共有することは未定義の動作です。フロントエンドは、LLVM IR を生成する際にこれらの制限を遵守する必要があります。
ランタイムの SVE ベクター長が変わる可能性があっても、LLVM IR および CodeGen のほとんどすべての部分では、ランタイムの vscale
の値は変更されないと仮定できます。コンパイラが呼び出し境界の周りに適切な smstart
および smstop
命令を挿入する場合、SVE 状態への影響を軽減できます。状態の変更を呼び出しの周りのごく短いウィンドウに限定することで、操作のスケジュール方法と、状態遷移間でライブ値がどのように保持されるかを制御できます。
このレベルの粒度で PSTATE.SM を制御するために、イントリンシックではなく、関数と呼び出しサイトの属性を使用します。
属性の制限¶
異なる SVE ベクター長を使用する可能性がある関数との間で、スケーラブルベクターオブジェクト(へのポインター)を渡したり、返したりすることは未定義の動作です。これには、ストリーミングインターフェイスではないが、
aarch64_pstate_sm_body
でマークされた関数が含まれます。関数を
aarch64_pstate_sm_compatible
とaarch64_pstate_sm_enabled
の両方で装飾することは許可されていません。関数を次の属性のうち複数で装飾することは許可されていません:
aarch64_new_za
,aarch64_in_za
,aarch64_out_za
,aarch64_inout_za
,aarch64_preserves_za
。
これらの制限は、より高レベルの SME ACLE にも適用されます。つまり、Clang で診断をエミットして、ユーザーに誤った動作を知らせることができます。
コンパイラが挿入するストリーミングモードの変更¶
以下の表は、異なる属性を持つ関数間の呼び出しを行う際に、コンパイラが考慮する必要がある PSTATE.SM の遷移について説明しています。この表では、次の省略形を使用します。
N
通常のインターフェイスを持つ関数(エントリ時に PSTATE.SM=0、リターン時に PSTATE.SM=0)
S
ストリーミングインターフェイスを持つ関数(エントリ時に PSTATE.SM=1、リターン時に PSTATE.SM=1)
SC
ストリーミング互換インターフェイスを持つ関数(エントリ時に PSTATE.SM は 0 または 1 のいずれかになり、リターン時には変更されません)。
__attribute__((arm_locally_streaming))
を持つ関数は、呼び出し側にとって属性が「ストリーミング」と同義であり、呼び出し先にとっては呼び出し側に明示的に公開されていない実装の詳細にすぎないため、この表から除外されています。
From |
To |
呼び出し前 |
呼び出し後 |
例外後 |
---|---|---|---|---|
N |
N |
|||
N |
S |
SMSTART |
SMSTOP |
|
N |
SC |
|||
S |
N |
SMSTOP |
SMSTART |
SMSTART |
S |
S |
SMSTART |
||
S |
SC |
SMSTART |
||
SC |
N |
呼び出し前の PSTATE.SM が 1 の場合、SMSTOP |
呼び出し前の PSTATE.SM が 1 の場合、SMSTART |
呼び出し前の PSTATE.SM が 1 の場合、SMSTART |
SC |
S |
呼び出し前の PSTATE.SM が 0 の場合、SMSTART |
呼び出し前の PSTATE.SM が 0 の場合、SMSTOP |
呼び出し前の PSTATE.SM が 1 の場合、SMSTART |
SC |
SC |
呼び出し前の PSTATE.SM が 1 の場合、SMSTART |
PSTATE.SM の変更により FP/ベクターレジスタがゼロになるため、レジスタ割り当ての前に smstart
および smstop
命令を発行して、レジスタアロケータがモード変更の周りでレジスタをスピル/リロードできるようにすることをお勧めします。
また、コンパイラは、どの操作が呼び出し/関数の引数/結果の一部であり、どの操作が関数の本体の一部であるかに関する十分な情報を持っている必要があります。これにより、モード変更を正確な位置に配置できます。これを行うのに適切な場所は SelectionDAG のようです。SelectionDAG では、呼び出しの引数/戻り値を低レベルに変換して、指定された呼び出し規約を実装します。SelectionDAG は、操作の順序を指定し、命令のスケジューリングを事前に制御するためのチェーンとグルーを提供します。
状態を保持する例¶
通常のインターフェイスを持つ関数からストリーミングインターフェイスを持つ関数に float
値を渡したり返したりする場合、呼び出しサイトは、引数/結果のレジスタが保持されており、smstart/smstop
と呼び出しの間に他のコードがスケジュールされていないことを保証する必要があります。
define float @foo(float %f) nounwind {
%res = call float @bar(float %f) "aarch64_pstate_sm_enabled"
ret float %res
}
declare float @bar(float) "aarch64_pstate_sm_enabled"
プログラムは、レジスタ s0
の浮動小数点引数と戻り値の値を保持する必要があります。
foo: // @foo
// %bb.0:
stp d15, d14, [sp, #-80]! // 16-byte Folded Spill
stp d13, d12, [sp, #16] // 16-byte Folded Spill
stp d11, d10, [sp, #32] // 16-byte Folded Spill
stp d9, d8, [sp, #48] // 16-byte Folded Spill
str x30, [sp, #64] // 8-byte Folded Spill
str s0, [sp, #76] // 4-byte Folded Spill
smstart sm
ldr s0, [sp, #76] // 4-byte Folded Reload
bl bar
str s0, [sp, #76] // 4-byte Folded Spill
smstop sm
ldp d9, d8, [sp, #48] // 16-byte Folded Reload
ldp d11, d10, [sp, #32] // 16-byte Folded Reload
ldp d13, d12, [sp, #16] // 16-byte Folded Reload
ldr s0, [sp, #76] // 4-byte Folded Reload
ldr x30, [sp, #64] // 8-byte Folded Reload
ldp d15, d14, [sp], #80 // 16-byte Folded Reload
ret
ISD ノードに正しいレジスタマスクを設定し、適切な場所に smstart/smstop
を挿入することで、これが正しく行われるようにする必要があります。
命令選択ノード¶
AArch64ISD::SMSTART Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]
AArch64ISD::SMSTOP Chain, [SM|ZA|Both], CurrentState, ExpectedState[, RegMask]
SMSTART/SMSTOP
ノードは、条件付き SMSTART/SMSTOP の場合に CurrentState
および ExpectedState
オペランドを受け取ります。命令は、CurrentState != ExpectedState の場合にのみ実行されます。
CurrentState
と ExpectedState
がコンパイル時に評価できる場合(つまり、両方とも定数である場合)、無条件の smstart/smstop
命令が発行されます。それ以外の場合、ノードは、比較/分岐と smstart/smstop
に展開される疑似命令に一致します。これは、SC -> N
および SC -> S
からの遷移を実装するために必要です。
アンチェーンド関数呼び出し¶
「aarch64_pstate_sm_enabled
」を持つ関数がストリーミング互換ではない関数を呼び出す場合、コンパイラは呼び出しの前に SMSTOP を挿入し、呼び出しの後に SMSTOP を挿入する必要があります。
呼び出される関数が副作用がなく、関数呼び出しに低レベル変換されるイントリンシック(例: @llvm.cos()
)である場合、@llvm.cos()
の呼び出しはどのチェーンにも属していません。自由にスケジュールできます。
コールサイトの低レベル変換では、次のノードの小さなチェーンが作成されます。
呼び出しシーケンスを開始する
仮想レジスタから ABI で指定された物理レジスタに値をコピーする
分岐とリンクを実行する
コールシーケンスを停止します。
出力値を物理レジスタから仮想レジスタにコピーします。
コールサイトのChainが使用されていない場合、チェーンされたシーケンスからの結果値のみが使用されますが、Chain自体は破棄されます。
SMSTART
ノードとSMSTOP
ノードはChainを返しますが、実際の値は返しません。したがって、SMSTART/SMSTOP
ノードが使用されないChainの一部である場合、これらのノードはスケジューリングの対象とならず、DAGから削除されます。これらのノードが削除されるのを防ぐために、CopyFromReg
からの結果がSMSTART/SMSTOP
の実行後にのみ使用できるようにする必要があります。
これには、CopyToReg -> CopyFromRegのシーケンスを使用できます。これにより、値が仮想レジスタに移動/移動され、これらのノードがSMSTART/SMSTOPとチェーンされて、結果値を計算する式の一部になります。結果のCOPYノードは、レジスタアロケータによって削除されます。
以下の例は、Chainによって結果がリンクされておらず、値によってリンクされているDAGでこれがどのように使用されるかを示しています。
t0: ch,glue = AArch64ISD::SMSTOP ...
t1: ch,glue = ISD::CALL ....
t2: res,ch,glue = CopyFromReg t1, ...
t3: ch,glue = AArch64ISD::SMSTART t2:1, .... <- this is now part of the expression that returns the result value.
t4: ch = CopyToReg t3, Register:f64 %vreg, t2
t5: res,ch = CopyFromReg t4, Register:f64 %vreg
t6: res = FADD t5, t9
関数開始時にDAGにSMSTART
を挿入する必要があるローカルストリーミング関数でも、これが必要です。
__attribute__((arm_locally_streaming)) を持つ関数¶
関数がarm_locally_streaming
としてマークされている場合、プロローグ/エピローグのランタイムSVEベクター長は、関数の本体のベクター長と異なる場合があります。これは、スタックフレームを設定した後にsmstartを呼び出し、同様にスタックフレームの割り当てを解除する前にsmstopを呼び出すためです。
ローカル変数を割り当てるために正しいSVEベクター長を使用することを確実にするために、CPUがまだストリーミングモードになっていない場合でも、ADDSVL
命令を使用してストリーミングベクター長を使用してスタックスロットを割り当てることができます。
これはローカル変数でのみ機能し、呼び出し先保存スロットでは機能しません。これは、LLVMが1つのスタックフレームで2つの異なるスケーラブルベクター長を混在させることをサポートしていないためです。つまり、関数がarm_locally_streaming
としてマークされており、プロローグでSVE呼び出し先保存をスピルする必要がある場合は、現在サポートされていません。ただし、arm_locally_streaming
関数はベクター長に依存する値を受け取ったり返したりできないため、ユーザーが介入しない限り、これは発生しにくいです。それ以外の場合は、'aarch64_sve_pcs
'を使用してSVE PCSを強制し、さらにarm_locally_streaming
を使用して、この問題が発生する必要があります。この組み合わせは、Clangで診断を発行することで防止できます。
arm_locally_streaming
属性を持つ関数のプロローグ/エピローグがどのように見えるかの例を以下に示します。
#define N 64
void __attribute__((arm_streaming_compatible)) some_use(svfloat32_t *);
// Use a float argument type, to check the value isn't clobbered by smstart.
// Use a float return type to check the value isn't clobbered by smstop.
float __attribute__((noinline, arm_locally_streaming)) foo(float arg) {
// Create local for SVE vector to check local is created with correct
// size when not yet in streaming mode (ADDSVL).
float array[N];
svfloat32_t vector;
some_use(&vector);
svst1_f32(svptrue_b32(), &array[0], vector);
return array[N - 1] + arg;
}
スタック領域の割り当てにはADDSVLを使用し、戻り値/引数の値を上書きすることを避ける必要があります。
_Z3foof: // @_Z3foof
// %bb.0: // %entry
stp d15, d14, [sp, #-96]! // 16-byte Folded Spill
stp d13, d12, [sp, #16] // 16-byte Folded Spill
stp d11, d10, [sp, #32] // 16-byte Folded Spill
stp d9, d8, [sp, #48] // 16-byte Folded Spill
stp x29, x30, [sp, #64] // 16-byte Folded Spill
add x29, sp, #64
str x28, [sp, #80] // 8-byte Folded Spill
addsvl sp, sp, #-1
sub sp, sp, #256
str s0, [x29, #28] // 4-byte Folded Spill
smstart sm
sub x0, x29, #64
addsvl x0, x0, #-1
bl _Z10some_usePu13__SVFloat32_t
sub x8, x29, #64
ptrue p0.s
ld1w { z0.s }, p0/z, [x8, #-1, mul vl]
ldr s1, [x29, #28] // 4-byte Folded Reload
st1w { z0.s }, p0, [sp]
ldr s0, [sp, #252]
fadd s0, s0, s1
str s0, [x29, #28] // 4-byte Folded Spill
smstop sm
ldr s0, [x29, #28] // 4-byte Folded Reload
addsvl sp, sp, #1
add sp, sp, #256
ldp x29, x30, [sp, #64] // 16-byte Folded Reload
ldp d9, d8, [sp, #48] // 16-byte Folded Reload
ldp d11, d10, [sp, #32] // 16-byte Folded Reload
ldp d13, d12, [sp, #16] // 16-byte Folded Reload
ldr x28, [sp, #80] // 8-byte Folded Reload
ldp d15, d14, [sp], #96 // 16-byte Folded Reload
ret
ストリーミングモードでの不正な命令の使用を防ぐ¶
ストリーミングモード(PSTATE.SM=1)でプログラムを実行する場合、SVE/SVE2命令のサブセットと、ほとんどのAdvSIMD/NEON命令は無効です。
通常モード(PSTATE.SM=0)でプログラムを実行する場合、SME命令のサブセットは無効です。
ストリーミング互換関数は、PSTATE.SM=0またはPSTATE.SM=1のいずれの場合でも有効な命令のみを使用する必要があります。
PSTATE.SMの値は、フィーチャーフラグではなく、関数属性によって制御されます。これは、'+sme
'に対してコンパイルでき、要求されたストリーミングモードでは無効な場合でも、コンパイラーが任意の命令をコード生成することを意味します。コンパイラーは、ランタイムで特定の操作が利用可能であるという前提でコンパイラーが変換を行わないように、関数属性を使用する必要があります。
フィーチャーフラグでこれをモデル化しないことに意識的な選択をしました。これは、どちらのモードでもインラインアセンブリ(ユーザーがsmstart/smstopを手動で配置する)を引き続きサポートしたいためであり、TableGenの制限のため、個々の命令レベルで実装することが非常に複雑になったためです(D120261とD121208を参照してください)。
最初のステップとして、関数にaarch64_pstate_sm_enabled
、aarch64_pstate_sm_body
、またはaarch64_pstate_sm_compatible
属性のいずれかがある場合、ベクター命令の使用を避けるために、ベクトル化(LoopVectorize/SLP)を完全に無効にします。
後で、ストリーミング互換命令のサブセットを使用したスケーラブルな自動ベクトル化を有効にするために、これらの制限を緩和することを目指しますが、そのためにはCostModel、Legalization、SelectionDAGの低減に対する変更が必要です。
また、関数にストリーミングモード属性が設定されている場合、Clangで診断を発行して、非ストリーミング(互換ではない)操作(ACLEイントリンシックなどを使用)の使用を防ぎます。
その他の考慮事項¶
コールサイトがPSTATE.SMを切り替える必要がある場合、または呼び出し先の関数本体が呼び出し元とは異なるストリーミングモードで実行される場合は、インライン化を無効にする必要があります。これは、関数呼び出しがストリーミングモードの変更の境界であるため必要です。
テールコール最適化は、呼び出しサイトがPSTATE.SMを切り替える必要がある場合、呼び出し元がPSTATE.SMの元の値を復元できるように無効にする必要があります。
3. PSTATE.ZA の処理¶
PSTATE.SMとは対照的に、PSTATE.ZAを有効にしても、SVEベクター長には影響せず、FP/AdvSIMD/SVEレジスタを上書きすることもありません。つまり、イントリンシックを使用してPSTATE.ZAを切り替えても安全です。これにより、プライベートZA関数(ZA状態を直接的または間接的に上書きする可能性のある関数)の呼び出しに対する遅延保存メカニズムの設定が簡単になります。
aarch64_new_za
でマークされた関数を処理するために、SelectionDAGの直前に実行される新しいLLVM IRパス(SMEABIPass)を導入しました。このパスで処理されたこのような関数には、aarch64_expanded_pstate_za
がマークされます。
遅延保存の設定¶
遅延保存のコミット¶
例外処理とZA¶
4. 型¶
AArch64 Predicate-as-Counter 型¶
- 概要:
predicate-as-counter型は、AArch64 SVE述語レジスタに保持されているpredicate-as-counter値の型を表します。このような値には、アクティブなレーン数、要素幅、および生成されたマスクを反転する必要があるかどうかを示すビットに関する情報が含まれています。ACLEイントリンシックを使用して、predicate-as-counter値を述語ベクトルとの間で移動する必要があります。
型には特定の制限があります。
型は、関数のパラメーターと戻り値に使用できます。
この型でサポートされるLLVM操作は、
load
、store
、phi
、select
、およびalloca
命令に限定されます。
predicate-as-counter型は、スケーラブルな型です。
- 構文:
target("aarch64.svcount")