InAlloca 屬性之設計與使用¶
簡介¶
inalloca 屬性旨在允許取得透過記憶體傳遞的聚合參數的地址。此功能主要用於與 Microsoft C++ ABI 相容。在該 ABI 下,以傳值方式傳遞的類別實例會直接建構在參數堆疊記憶體中。在新增 inalloca 之前,LLVM 中的呼叫是不可分割的指令。在第一次堆疊調整和最終控制權轉移之間,沒有辦法執行中間工作,例如物件建構。使用 inalloca,所有在記憶體中傳遞的參數都被建模為單個 alloca,可以在呼叫之前將其儲存到其中。遺憾的是,這個複雜的功能附帶了一系列限制,旨在限制參數記憶體在呼叫期間的生命週期。
目前,建議前端和最佳化器避免產生此構造,主要因為它強制使用基底指標。此功能在未來可能會發展為允許通用的中級最佳化,但就目前而言,它應該被認為比使用複製傳值效率低。
預期用途¶
以下範例是在 32 位元 Microsoft C++ ABI 中將兩個預設建構的 Foo
物件傳遞給 g
的預期 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 配置參數堆疊空間並呼叫預設建構函式。預設建構函式可能會引發例外狀況,因此前端必須建立一個 landing pad。前端必須在恢復堆疊指標之前銷毀已建構的參數 b
。如果建構函式沒有展開,則會呼叫 g
。在 Microsoft C++ ABI 中,g
會銷毀其參數,然後在 f
中恢復堆疊。
設計考量¶
生命週期¶
此功能最大的設計考量是物件生命週期。我們無法將參數建模為 entry 區塊中的靜態 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 值寫入 undef,而不是堆疊調整。前端應該仍然恢復堆疊指標以避免堆疊洩漏。
例外狀況¶
也有可能發生例外狀況。如果參數評估或複製建構引發例外狀況,則著陸區塊必須執行清理,其中包括調整堆疊指標以避免堆疊洩漏。這表示堆疊記憶體的清理不能與呼叫本身綁定。需要有一個獨立的 IR 級指令來執行獨立的參數清理。
效率¶
最終,應該可以為此建構產生高效的程式碼。特別是,使用 inalloca 不應該需要基底指標。如果後端可以證明 CFG 中的所有點只有一個可能的堆疊級別,那麼它可以直接從堆疊指標訪問堆疊。雖然這尚未實現,但計劃是 inalloca 屬性不應有太大變化,但前端 IR 生成建議可能會更改。