InAlloca 属性的设计与用法¶
简介¶
inalloca 属性旨在允许获取通过内存按值传递的聚合参数的地址。 主要地,此功能是与 Microsoft C++ ABI 兼容所必需的。 在该 ABI 下,按值传递的类实例直接构造到参数堆栈内存中。 在添加 inalloca 之前,LLVM 中的调用是不可分割的指令。 在第一次堆栈调整和最终控制转移之间,无法执行中间工作,例如对象构造。 使用 inalloca,所有在内存中传递的参数都被建模为单个 alloca,可以在调用之前存储到其中。 不幸的是,这个复杂的功能带有一系列旨在限制调用周围参数内存生命周期的限制。
目前,建议前端和优化器避免生成此构造,主要是因为它强制使用基指针。 此功能将来可能会扩展以允许通用的中级优化,但就目前而言,它应被视为不如通过复制按值传递有效。
预期用途¶
下面的示例是将一些 C++ 代码降低为 LLVM IR 的预期方式,该代码在 32 位 Microsoft C++ ABI 中将两个默认构造的 Foo
对象传递给 g
。
// 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
中恢复堆栈。
设计考虑¶
生命周期¶
此功能最大的设计考虑是对象生命周期。 我们不能将参数建模为入口块中的静态 allocas,因为所有调用都需要使用堆栈顶部的内存来传递参数。 我们不能在函数入口处出售指向该内存的指针,因为在代码生成之后它们会别名。
反对在参数分配和调用站点之间使用 allocas 的规则避免了这个问题,但它创建了一个清理问题。 清理和生命周期通过堆栈保存和恢复调用显式处理。 未来,我们可能希望引入一个新的构造,例如 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 上,所有方法和许多其他函数都会调整堆栈以清除用于传递其参数的内存。 从某种意义上说,这意味着 allocas 会被调用自动清除。 但是,LLVM 将此建模为向传递给调用的所有 inalloca 值写入 undef,而不是堆栈调整。 前端仍然应该恢复堆栈指针以避免堆栈泄漏。
异常¶
还存在异常的可能性。 如果参数求值或复制构造抛出异常,则着陆区必须进行清理,包括调整堆栈指针以避免堆栈泄漏。 这意味着堆栈内存的清理不能与调用本身相关联。 需要一个单独的 IR 级别指令,可以执行独立的参数清理。
效率¶
最终,应该可以为此构造生成高效的代码。 特别是,使用 inalloca 不应要求基指针。 如果后端可以证明 CFG 中的所有点只有一个可能的堆栈级别,那么它可以直接从堆栈指针寻址堆栈。 虽然这尚未实现,但计划是 inalloca 属性不应发生太大变化,但前端 IR 生成建议可能会发生变化。