InAlloca 属性的设计和使用

简介

inalloca 属性旨在允许获取通过内存按值传递的聚合参数的地址。主要地,此功能是为与 Microsoft C++ ABI 的兼容性而设计的。在该 ABI 下,按值传递的类实例直接构造到参数栈内存中。在添加 inalloca 之前,LLVM 中的调用是不可分割的指令。在第一次栈调整和最终控制转移之间,没有办法执行中间工作,例如对象构造。使用 inalloca,所有以内存方式传递的参数都被建模为单个 alloca,可以在调用之前将其存储到该 alloca 中。不幸的是,此复杂功能附带了一系列旨在将参数内存的生命周期限制在调用周围的限制。

目前,建议前端和优化器避免生成此构造,主要是因为它强制使用基指针。此功能将来可能会扩展以允许通用中级优化,但目前,应将其视为比使用复制按值传递效率更低。

预期用法

下面的示例是某些 C++ 代码的预期 LLVM IR 降低,这些代码将两个默认构造的 Foo 对象传递给 32 位 Microsoft C++ ABI 中的 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 中恢复栈。

设计考虑

生命周期

此功能最大的设计考虑因素是对象生命周期。我们不能将参数建模为入口块中的静态 alloca,因为所有调用都需要使用栈顶部的内存来传递参数。我们不能在函数入口处提供指向该内存的指针,因为在代码生成之后它们将存在别名。

在参数分配和调用站点之间禁止 alloca 避免了此问题,但它创建了清理问题。清理和生命周期使用栈保存和恢复调用显式处理。将来,我们可能希望引入一个新的构造,例如 freeaafree,以明确表示此栈调整清理功能不如完整的栈保存和恢复功能强大。

嵌套调用和复制省略

我们还希望能够支持将复制省略到这些参数槽中。这意味着我们必须支持多个活动参数分配。

考虑以下评估:

// 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 生成建议可能会改变。