InAlloca属性の設計と使用方法¶
はじめに¶
inalloca属性は、値渡しでメモリ経由で渡される集約引数のアドレスを取得することを可能にするために設計されています。主に、この機能はMicrosoft C++ ABIとの互換性のために必要です。このABIでは、値渡しで渡されるクラスインスタンスは、引数のスタックメモリに直接構築されます。inallocaの追加前は、LLVMの呼び出しは不可分な命令でした。最初のスタック調整と最終的な制御転送の間で、オブジェクトの構築などの中間作業を実行する方法はありませんでした。inallocaを使用すると、メモリで渡されるすべての引数は単一のallocaとしてモデル化され、呼び出し前に格納できます。残念ながら、この複雑な機能には、呼び出し周辺の引数メモリの寿命を制限するように設計された多くの制限が伴います。
現時点では、フロントエンドとオプティマイザは、この構成を作成しないことをお勧めします。これは主に、ベースポインタの使用を強制するためです。この機能は将来、一般的なミドルレベルの最適化を可能にするように拡張される可能性がありますが、現時点では、コピーによる値渡しよりも効率が悪いと考える必要があります。
意図された使用方法¶
以下の例は、32ビットMicrosoft C++ ABIで2つのデフォルト構築されたFoo
オブジェクトをg
に渡すC++コードの意図されたLLVM IRローワーリングです。
// Foo is non-trivial.
struct Foo { int a, b; Foo(); ~Foo(); Foo(const Foo &); };
void g(Foo a, Foo b);
void f() {
g(Foo(), Foo());
}
%struct.Foo = type { i32, i32 }
declare void @Foo_ctor(%struct.Foo* %this)
declare void @Foo_dtor(%struct.Foo* %this)
declare void @g(<{ %struct.Foo, %struct.Foo }>* inalloca %memargs)
define void @f() {
entry:
%base = call i8* @llvm.stacksave()
%memargs = alloca <{ %struct.Foo, %struct.Foo }>
%b = getelementptr <{ %struct.Foo, %struct.Foo }>* %memargs, i32 1
call void @Foo_ctor(%struct.Foo* %b)
; If a's ctor throws, we must destruct b.
%a = getelementptr <{ %struct.Foo, %struct.Foo }>* %memargs, i32 0
invoke void @Foo_ctor(%struct.Foo* %a)
to label %invoke.cont unwind %invoke.unwind
invoke.cont:
call void @g(<{ %struct.Foo, %struct.Foo }>* inalloca %memargs)
call void @llvm.stackrestore(i8* %base)
...
invoke.unwind:
call void @Foo_dtor(%struct.Foo* %b)
call void @llvm.stackrestore(i8* %base)
...
}
スタックリークを回避するために、フロントエンドはllvm.stacksaveへの呼び出しを使用して現在のスタックポインタを保存します。次に、allocaを使用して引数のスタック領域を割り当て、デフォルトコンストラクタを呼び出します。デフォルトコンストラクタは例外をスローする可能性があるため、フロントエンドはランディングパッドを作成する必要があります。フロントエンドは、スタックポインタを復元する前に、既に構築されている引数b
を破棄する必要があります。コンストラクタがアンワインドしない場合、g
が呼び出されます。Microsoft C++ ABIでは、g
は引数を破棄し、その後f
でスタックが復元されます。
設計上の考慮事項¶
ライフタイム¶
この機能の最大の設計上の考慮事項はオブジェクトのライフタイムです。すべての呼び出しで引数を渡すためにスタックの一番上のメモリを使用する必要があるため、エントリブロックで引数を静的allocaとしてモデル化することはできません。コード生成後、それらがエイリアスされるため、関数エントリでそのメモリへのポインタを供給することはできません。
引数の割り当てと呼び出しサイト間のallocaに関する規則は、この問題を回避しますが、クリーンアップの問題が発生します。クリーンアップとライフタイムは、スタックの保存と復元呼び出しで明示的に処理されます。将来、freea
またはafree
などの新しい構成を導入して、このスタック調整クリーンアップが完全なスタックの保存と復元よりも強力ではないことを明確にする必要があるかもしれません。
ネストされた呼び出しとコピーの省略¶
これらの引数スロットへのコピーの省略もサポートできるようにしたいと考えています。つまり、複数のライブ引数割り当てをサポートする必要があります。
以下の評価を考えてみましょう。
// Foo is non-trivial.
struct Foo { int a; Foo(); Foo(const &Foo); ~Foo(); };
Foo bar(Foo b);
int main() {
bar(bar(Foo()));
}
この場合、bar
の引数スロットへのコピーを省略できるようにしたいと考えています。つまり、同時に複数の引数フレームセットをアクティブにする必要があります。まず、外部呼び出しのフレームを割り当てて、それを中間呼び出しへの非表示の構造体戻り値ポインタとして渡す必要があります。次に、中間呼び出しについても同様に、フレームを割り当ててそのアドレスをFoo
のデフォルトコンストラクタに渡します。内部bar
の評価をスタックの保存と復元でラップすることにより、複数の重複するアクティブな呼び出しフレームを持つことができます。
呼び出し元クリーンアップ呼び出し規約¶
もう一つの問題は、呼び出し元クリーンアップ規約の存在です。Windowsでは、すべてのメソッドと多くの他の関数は、引数を渡すために使用されるメモリをクリアするためにスタックを調整します。ある意味、これはallocaが呼び出しによって自動的にクリアされることを意味します。しかし、LLVMは代わりに、スタック調整ではなく、呼び出しに渡されたすべてのinalloca値への未定義の書き込みとしてこれをモデル化します。スタックリークを回避するために、フロントエンドは引き続きスタックポインタを復元する必要があります。
例外¶
例外が発生する可能性もあります。引数の評価またはコピーコンストラクションが例外をスローした場合、ランディングパッドはクリーンアップを行う必要があり、これにはスタックリークを回避するためのスタックポインタの調整が含まれます。つまり、スタックメモリのクリーンアップは、呼び出し自体に結び付けることはできません。引数の独立したクリーンアップを実行できる、別個のIRレベルの命令が必要です。
効率¶
最終的には、この構成に対して効率的なコードを生成することが可能になるはずです。特に、inallocaを使用しても、ベースポインタは必要ありません。バックエンドがCFG内のすべてのポイントに1つのスタックレベルしかないことを証明できれば、スタックポインタから直接スタックにアドレス指定できます。これはまだ実装されていませんが、inalloca属性はそれほど変更されませんが、フロントエンドのIR生成の推奨事項は変更される可能性があります。