LLVM 中的协程¶
警告
不保证跨 LLVM 版本的兼容性。
介绍¶
LLVM 协程是具有一个或多个挂起点的函数。当到达挂起点时,协程的执行会被挂起,控制权返回给其调用者。挂起的协程可以被恢复以从最后一个挂起点继续执行,或者可以被销毁。
在以下示例中,我们调用函数 f(它本身可能是也可能不是协程),该函数返回一个指向挂起协程的句柄(协程句柄),main 使用该句柄来恢复协程两次,然后销毁它
define i32 @main() {
entry:
%hdl = call ptr @f(i32 4)
call void @llvm.coro.resume(ptr %hdl)
call void @llvm.coro.resume(ptr %hdl)
call void @llvm.coro.destroy(ptr %hdl)
ret i32 0
}
除了协程执行时存在的功能堆栈帧之外,还有一个额外的存储区域,用于存储在协程挂起时保持协程状态的对象。这个存储区域被称为协程帧。它在协程被调用时创建,并在协程运行完成或在挂起时被销毁时销毁。
LLVM 目前支持两种样式的协程降低。这些样式支持的功能集、ABI 和前端代码生成模式都大相径庭。然而,这些样式也有很多共同之处。
在所有情况下,LLVM 协程最初都表示为一个普通的 LLVM 函数,该函数调用协程 intrinsic 来定义协程的结构。然后,在最一般的情况下,协程函数会被协程降低 Pass 重写,成为“启动函数”,即协程的初始入口点,它会执行直到首次到达挂起点。原始协程函数的其余部分被拆分到一些“恢复函数”中。任何必须跨挂起持久化的状态都存储在协程帧中。恢复函数必须能够处理“正常”恢复(继续协程的正常执行)或“异常”恢复(必须在不尝试挂起协程的情况下展开协程)。
切换恢复降低¶
在 LLVM 的标准切换恢复降低中,通过使用 llvm.coro.id 发出信号,协程帧存储为“协程对象”的一部分,该对象表示特定协程调用的句柄。所有协程对象都支持通用的 ABI,允许在不了解协程实现的任何信息的情况下使用某些功能
可以使用 llvm.coro.done 查询协程对象,以查看它是否已完成。
如果协程对象尚未完成,可以使用 llvm.coro.resume 正常恢复。
可以使用 llvm.coro.destroy 销毁协程对象,使协程对象失效。即使协程已正常完成,也必须单独执行此操作。
“Promise” 存储(已知具有特定大小和对齐方式)可以使用 llvm.coro.promise 从协程对象中投影出来。协程实现必须已编译为定义相同大小和对齐方式的 promise。
通常,在协程对象运行时以任何这些方式与之交互都具有未定义的行为。
协程函数被拆分为三个函数,代表控制权可以进入协程的三种不同方式
最初调用的启动函数,它接受任意参数并返回指向协程对象的指针;
当协程恢复时调用的协程恢复函数,它接受指向协程对象的指针并返回 void;
当协程被销毁时调用的协程销毁函数,它接受指向协程对象的指针并返回 void。
由于恢复和销毁函数在所有挂起点之间共享,因此挂起点必须将活动挂起的索引存储在协程对象中,并且恢复/销毁函数必须切换该索引以返回到正确的点。因此得名这种降低方式。
指向恢复和销毁函数的指针存储在协程对象中,位于所有协程都固定的已知偏移量处。已完成的协程用空恢复函数表示。
存在一个略微复杂的 intrinsic 协议,用于分配和释放协程对象。它的复杂性是为了允许由于内联而省略分配。此协议将在下面进一步详细讨论。
前端可以生成代码来直接调用协程函数;这将变成对启动函数的调用,并将返回指向协程对象的指针。前端应始终使用相应的 intrinsic 恢复或销毁协程。
返回延续降低¶
在返回延续降低中,通过使用 llvm.coro.id.retcon 或 llvm.coro.id.retcon.once 发出信号,ABI 的某些方面必须由前端更明确地处理。
在这种降低中,每个挂起点都接受一个“yield 值”列表,这些值与函数指针(称为延续函数)一起返回给调用者。通过简单地调用此延续函数指针来恢复协程。原始协程被分为启动函数,然后是任意数量的这些延续函数,每个挂起点一个。
LLVM 实际上支持两种密切相关的返回延续降低
在正常的返回延续降低中,协程可以多次挂起自身。这意味着延续函数本身返回另一个延续指针以及一个 yield 值列表。
协程通过返回空延续指针来指示它已运行完成。任何 yield 值都将是 undef 并且应被忽略。
在单次 yield 返回延续降低中,协程必须恰好挂起自身一次(或抛出异常)。启动函数返回一个延续函数指针和 yield 值,当协程运行完成时,延续函数可以选择性地返回普通结果。
协程帧维护在一个固定大小的缓冲区中,该缓冲区传递给 coro.id intrinsic,它静态地保证了特定的大小和对齐方式。相同的缓冲区必须传递给延续函数。如果缓冲区不足,协程将分配内存,在这种情况下,它至少需要将该指针存储在缓冲区中;因此,缓冲区必须始终至少是指针大小的。协程如何使用缓冲区可能因挂起点而异。
除了缓冲区指针之外,延续函数还接受一个参数,指示协程是正常恢复(零)还是异常恢复(非零)。
在将返回延续协程完全内联到调用者之后,LLVM 目前无法有效地静态消除分配。如果 LLVM 的协程支持主要用于低级降低,并且预计内联将在管道中较早应用,则这可能是可以接受的。
异步降低¶
在异步延续降低中,通过使用 llvm.coro.id.async 发出信号,控制流的处理必须由前端显式处理。
在这种降低中,假定协程将当前的 异步上下文 作为其参数之一(参数位置由 llvm.coro.id.async 确定)。它用于编组协程的参数和返回值。因此,异步协程返回 void。
define swiftcc void @async_coroutine(ptr %async.ctxt, ptr, ptr) {
}
跨挂起点存活的值需要存储在协程帧中,以便在延续函数中可用。此帧存储为 异步上下文 的尾部。
每个挂起点都采用 上下文投影函数 参数,该参数描述如何获取延续 异步上下文,并且每个挂起点都有一个关联的 恢复函数,由 llvm.coro.async.resume intrinsic 表示。通过调用此 恢复函数 并将 异步上下文 作为其参数之一传递来恢复协程。恢复函数 可以通过应用由前端作为 llvm.coro.suspend.async intrinsic 的参数提供的 上下文投影函数 来恢复其(调用者的)异步上下文。
// For example:
struct async_context {
struct async_context *caller_context;
...
}
char *context_projection_function(struct async_context *callee_ctxt) {
return callee_ctxt->caller_context;
}
%resume_func_ptr = call ptr @llvm.coro.async.resume()
call {ptr, ptr, ptr} (ptr, ptr, ...) @llvm.coro.suspend.async(
ptr %resume_func_ptr,
ptr %context_projection_function
前端应通过 llvm.coro.id.async 的参数提供与每个异步协程关联的 异步函数指针 结构。 异步上下文 的初始大小和对齐方式必须作为 llvm.coro.id.async intrinsic 的参数提供。降低将使用协程帧要求更新大小条目。前端负责为 异步上下文 分配内存,但可以使用 异步函数指针 结构来获取所需的大小。
struct async_function_pointer {
uint32_t relative_function_pointer_to_async_impl;
uint32_t context_size;
}
降低会将异步协程拆分为一个启动函数和每个挂起点一个恢复函数。
控制流如何在调用者、挂起点和返回恢复函数之间传递取决于前端。
挂起点采用一个函数及其参数。该函数旨在模拟转移到被调用函数。它将被降低尾调用,因此必须具有与异步协程相同的签名和调用约定。
call {ptr, ptr, ptr} (ptr, ptr, ...) @llvm.coro.suspend.async(
ptr %resume_func_ptr,
ptr %context_projection_function,
ptr %suspend_function,
ptr %arg1, ptr %arg2, i8 %arg3)
协程示例¶
以下示例均为切换恢复协程。
协程表示¶
让我们看一个 LLVM 协程的示例,其行为由以下伪代码草图表示。
void *f(int n) {
for(;;) {
print(n++);
<suspend> // returns a coroutine handle on first suspend
}
}
此协程使用值 n 作为参数调用某个函数 print 并挂起执行。每次此协程恢复时,它都会再次调用 print,参数比上次大 1。此协程永远不会自行完成,必须显式销毁。如果我们将此协程与上一节中显示的 main 一起使用。它将使用值 4、5 和 6 调用 print,之后协程将被销毁。
此协程的 LLVM IR 如下所示
define ptr @f(i32 %n) presplitcoroutine {
entry:
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
%size = call i32 @llvm.coro.size.i32()
%alloc = call ptr @malloc(i32 %size)
%hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
br label %loop
loop:
%n.val = phi i32 [ %n, %entry ], [ %inc, %loop ]
%inc = add nsw i32 %n.val, 1
call void @print(i32 %n.val)
%0 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %0, label %suspend [i8 0, label %loop
i8 1, label %cleanup]
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
call void @free(ptr %mem)
br label %suspend
suspend:
%unused = call i1 @llvm.coro.end(ptr %hdl, i1 false, token none)
ret ptr %hdl
}
entry 块建立协程帧。coro.size intrinsic 降低为表示协程帧所需大小的常量。coro.begin intrinsic 初始化协程帧并返回协程句柄。coro.begin 的第二个参数给定一块内存,如果需要动态分配协程帧,则使用该内存。
coro.id intrinsic 用作协程标识,在 coro.begin intrinsic 被跳转线程等优化 Pass 复制的情况下很有用。
cleanup 块销毁协程帧。 coro.free intrinsic,给定协程句柄,返回要释放的内存块的指针,如果协程帧不是动态分配的,则返回 null。当协程自行运行完成或通过调用 coro.destroy intrinsic 销毁时,将进入 cleanup 块。
suspend 块包含协程运行完成或挂起时要执行的代码。coro.end intrinsic 标记协程需要将控制权返回给调用者的点(如果它不是协程的初始调用)。
loop 块表示协程的主体。 coro.suspend intrinsic 与以下 switch 结合使用,指示当协程挂起(默认情况)、恢复(情况 0)或销毁(情况 1)时控制流会发生什么。
协程转换¶
协程降低的步骤之一是构建协程帧。分析定义-使用链以确定哪些对象需要跨挂起点保持活动状态。在上一节中显示的协程中,虚拟寄存器 %inc 的使用与定义被挂起点分隔,因此,它不能驻留在堆栈帧上,因为后者在协程挂起且控制权返回给调用者后就会消失。在协程帧中分配了一个 i32 插槽,并且 %inc 根据需要从该插槽溢出和重新加载。
我们还存储恢复和销毁函数的地址,以便当在编译时无法静态确定协程的标识时,coro.resume 和 coro.destroy intrinsic 可以恢复和销毁协程。对于我们的示例,协程帧将是
%f.frame = type { ptr, ptr, i32 }
在恢复和销毁部分被概述后,函数 f 将仅包含负责创建和初始化协程帧以及执行协程直到到达挂起点的代码
define ptr @f(i32 %n) {
entry:
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
%alloc = call noalias ptr @malloc(i32 24)
%frame = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
%1 = getelementptr %f.frame, ptr %frame, i32 0, i32 0
store ptr @f.resume, ptr %1
%2 = getelementptr %f.frame, ptr %frame, i32 0, i32 1
store ptr @f.destroy, ptr %2
%inc = add nsw i32 %n, 1
%inc.spill.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i32 0, i32 2
store i32 %inc, ptr %inc.spill.addr
call void @print(i32 %n)
ret ptr %frame
}
协程的概述恢复部分将驻留在函数 f.resume 中
define internal fastcc void @f.resume(ptr %frame.ptr.resume) {
entry:
%inc.spill.addr = getelementptr %f.frame, ptr %frame.ptr.resume, i64 0, i32 2
%inc.spill = load i32, ptr %inc.spill.addr, align 4
%inc = add i32 %inc.spill, 1
store i32 %inc, ptr %inc.spill.addr, align 4
tail call void @print(i32 %inc)
ret void
}
而函数 f.destroy 将包含协程的清理代码
define internal fastcc void @f.destroy(ptr %frame.ptr.destroy) {
entry:
tail call void @free(ptr %frame.ptr.destroy)
ret void
}
避免堆分配¶
特定的协程使用模式(在概述部分中的 main 函数中说明),其中协程由同一个调用函数创建、操作和销毁,对于实现 RAII 惯用法的协程来说很常见,并且适用于分配省略优化,该优化通过将其调用者中的静态 alloca 存储协程帧来避免动态分配。
在 entry 块中,我们将调用 coro.alloc intrinsic,当需要动态分配时,它将返回 true,如果省略动态分配,则返回 false。
entry:
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
%need.dyn.alloc = call i1 @llvm.coro.alloc(token %id)
br i1 %need.dyn.alloc, label %dyn.alloc, label %coro.begin
dyn.alloc:
%size = call i32 @llvm.coro.size.i32()
%alloc = call ptr @CustomAlloc(i32 %size)
br label %coro.begin
coro.begin:
%phi = phi ptr [ null, %entry ], [ %alloc, %dyn.alloc ]
%hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %phi)
在 cleanup 块中,我们将使释放协程帧取决于 coro.free intrinsic。如果省略分配,coro.free 返回 null,从而跳过释放代码
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
%need.dyn.free = icmp ne ptr %mem, null
br i1 %need.dyn.free, label %dyn.free, label %if.end
dyn.free:
call void @CustomFree(ptr %mem)
br label %if.end
if.end:
...
使用如上所述表示的分配和释放,在协程堆分配省略优化之后,生成的 main 将是
define i32 @main() {
entry:
call void @print(i32 4)
call void @print(i32 5)
call void @print(i32 6)
ret i32 0
}
多个挂起点¶
让我们考虑具有多个挂起点的协程
void *f(int n) {
for(;;) {
print(n++);
<suspend>
print(-n);
<suspend>
}
}
匹配的 LLVM 代码如下所示(其余代码与上一节中的代码保持不变)
loop:
%n.addr = phi i32 [ %n, %entry ], [ %inc, %loop.resume ]
call void @print(i32 %n.addr) #4
%2 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %2, label %suspend [i8 0, label %loop.resume
i8 1, label %cleanup]
loop.resume:
%inc = add nsw i32 %n.addr, 1
%sub = xor i32 %n.addr, -1
call void @print(i32 %sub)
%3 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %3, label %suspend [i8 0, label %loop
i8 1, label %cleanup]
在这种情况下,协程帧将包括一个挂起索引,该索引将指示协程需要在哪个挂起点恢复。
%f.frame = type { ptr, ptr, i32, i32 }
恢复函数将使用索引跳转到适当的基本块,并且看起来像这样
define internal fastcc void @f.Resume(ptr %FramePtr) {
entry.Resume:
%index.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i64 0, i32 2
%index = load i8, ptr %index.addr, align 1
%switch = icmp eq i8 %index, 0
%n.addr = getelementptr inbounds %f.Frame, ptr %FramePtr, i64 0, i32 3
%n = load i32, ptr %n.addr, align 4
br i1 %switch, label %loop.resume, label %loop
loop.resume:
%sub = sub nsw i32 0, %n
call void @print(i32 %sub)
br label %suspend
loop:
%inc = add nsw i32 %n, 1
store i32 %inc, ptr %n.addr, align 4
tail call void @print(i32 %inc)
br label %suspend
suspend:
%storemerge = phi i8 [ 0, %loop ], [ 1, %loop.resume ]
store i8 %storemerge, ptr %index.addr, align 1
ret void
}
如果不同的挂起点需要执行不同的清理代码,则 f.destroy 函数中将有类似的 switch。
注意
在协程状态中使用挂起索引并在 f.resume 和 f.destroy 中使用 switch 是一种可能的实现策略。我们探索了另一种选择,其中为每个挂起点创建不同的 f.resume1、f.resume2 等,并且不是存储索引,而是在每个挂起点更新恢复和销毁函数指针。早期测试表明,当前方法比后者更容易进行优化,因此它是目前实现的降低策略。
区分保存和挂起¶
在前面的示例中,设置恢复索引(或需要发生的其他状态更改以准备协程以进行恢复)与协程挂起同时发生。但是,在某些情况下,有必要控制何时准备协程以进行恢复以及何时挂起协程。
在以下示例中,协程表示由异步操作 async_op1 和 async_op2 的完成驱动的某些活动,这些异步操作获取协程句柄作为参数,并在异步操作完成后恢复协程。
void g() {
for (;;)
if (cond()) {
async_op1(<coroutine-handle>); // will resume once async_op1 completes
<suspend>
do_one();
}
else {
async_op2(<coroutine-handle>); // will resume once async_op2 completes
<suspend>
do_two();
}
}
}
在这种情况下,协程应在调用 async_op1 和 async_op2 之前准备好进行恢复。coro.save intrinsic 用于指示协程应准备好进行恢复的点(即,何时应在协程帧中存储恢复索引,以便可以在正确的恢复点恢复它)
if.true:
%save1 = call token @llvm.coro.save(ptr %hdl)
call void @async_op1(ptr %hdl)
%suspend1 = call i1 @llvm.coro.suspend(token %save1, i1 false)
switch i8 %suspend1, label %suspend [i8 0, label %resume1
i8 1, label %cleanup]
if.false:
%save2 = call token @llvm.coro.save(ptr %hdl)
call void @async_op2(ptr %hdl)
%suspend2 = call i1 @llvm.coro.suspend(token %save2, i1 false)
switch i8 %suspend2, label %suspend [i8 0, label %resume2
i8 1, label %cleanup]
协程 Promise¶
协程作者或前端可以指定一个特殊的 alloca,该 alloca 可用于与协程通信。这个特殊的 alloca 称为 协程 promise,并作为第二个参数提供给 coro.id intrinsic。
以下协程指定一个 32 位整数 promise,并使用它来存储协程生成的当前值。
define ptr @f(i32 %n) {
entry:
%promise = alloca i32
%id = call token @llvm.coro.id(i32 0, ptr %promise, ptr null, ptr null)
%need.dyn.alloc = call i1 @llvm.coro.alloc(token %id)
br i1 %need.dyn.alloc, label %dyn.alloc, label %coro.begin
dyn.alloc:
%size = call i32 @llvm.coro.size.i32()
%alloc = call ptr @malloc(i32 %size)
br label %coro.begin
coro.begin:
%phi = phi ptr [ null, %entry ], [ %alloc, %dyn.alloc ]
%hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %phi)
br label %loop
loop:
%n.val = phi i32 [ %n, %coro.begin ], [ %inc, %loop ]
%inc = add nsw i32 %n.val, 1
store i32 %n.val, ptr %promise
%0 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %0, label %suspend [i8 0, label %loop
i8 1, label %cleanup]
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
call void @free(ptr %mem)
br label %suspend
suspend:
%unused = call i1 @llvm.coro.end(ptr %hdl, i1 false, token none)
ret ptr %hdl
}
协程使用者可以依赖 coro.promise intrinsic 来访问协程 promise。
define i32 @main() {
entry:
%hdl = call ptr @f(i32 4)
%promise.addr = call ptr @llvm.coro.promise(ptr %hdl, i32 4, i1 false)
%val0 = load i32, ptr %promise.addr
call void @print(i32 %val0)
call void @llvm.coro.resume(ptr %hdl)
%val1 = load i32, ptr %promise.addr
call void @print(i32 %val1)
call void @llvm.coro.resume(ptr %hdl)
%val2 = load i32, ptr %promise.addr
call void @print(i32 %val2)
call void @llvm.coro.destroy(ptr %hdl)
ret i32 0
}
编译本节中的示例后,编译结果将是
define i32 @main() {
entry:
tail call void @print(i32 4)
tail call void @print(i32 5)
tail call void @print(i32 6)
ret i32 0
}
最终挂起¶
协程作者或前端可以将特定的挂起指定为最终挂起,方法是将 coro.suspend intrinsic 的第二个参数设置为 true。这样的挂起点具有两个属性
可以通过 coro.done intrinsic 检查挂起的协程是否处于最终挂起点;
恢复在最终挂起点停止的协程会导致未定义的行为。对于最终挂起点的协程,唯一可能的动作是通过 coro.destroy intrinsic 销毁它。
从用户的角度来看,最终挂起点代表协程到达结尾的概念。从编译器的角度来看,这是一个优化机会,可以减少恢复点的数量(以及恢复函数中的 switch 情况)。
以下是一个函数的示例,该函数不断恢复协程,直到到达最终挂起点,之后协程被销毁
define i32 @main() {
entry:
%hdl = call ptr @f(i32 4)
br label %while
while:
call void @llvm.coro.resume(ptr %hdl)
%done = call i1 @llvm.coro.done(ptr %hdl)
br i1 %done, label %end, label %while
end:
call void @llvm.coro.destroy(ptr %hdl)
ret i32 0
}
通常,最终挂起点是前端注入的挂起点,它不对应于高级语言的任何显式编写的挂起点。例如,对于只有一个挂起点的 Python 生成器
def coroutine(n):
for i in range(n):
yield i
Python 前端将注入另外两个挂起点,因此实际代码如下所示
void* coroutine(int n) {
int current_value;
<designate current_value to be coroutine promise>
<SUSPEND> // injected suspend point, so that the coroutine starts suspended
for (int i = 0; i < n; ++i) {
current_value = i; <SUSPEND>; // corresponds to "yield i"
}
<SUSPEND final=true> // injected final suspend point
}
而 python 迭代器 __next__ 看起来像
int __next__(void* hdl) {
coro.resume(hdl);
if (coro.done(hdl)) throw StopIteration();
return *(int*)coro.promise(hdl, 4, false);
}
自定义 ABI 和插件库¶
插件库可以扩展协程降低,使各种各样的用户能够利用协程转换 Pass。现有的协程降低通过以下方式扩展
定义从现有 ABI 继承的自定义 ABI,
在构造 CoroSplit Pass 时给出自定义 ABI 生成器的列表,以及
使用 coro.begin.custom.abi 代替 coro.begin,后者具有用于协程的生成器/ABI 索引的附加参数。
覆盖 SwitchABI 物化 的自定义 ABI 如下所示
class CustomSwitchABI : public coro::SwitchABI {
public:
CustomSwitchABI(Function &F, coro::Shape &S)
: coro::SwitchABI(F, S, ExtraMaterializable) {}
};
在构造 CoroSplit Pass 时给出自定义 ABI 生成器列表如下所示
CoroSplitPass::BaseABITy GenCustomABI = [](Function &F, coro::Shape &S) {
return std::make_unique<CustomSwitchABI>(F, S);
};
CGSCCPassManager CGPM;
CGPM.addPass(CoroSplitPass({GenCustomABI}));
使用自定义 ABI 的协程的 LLVM IR 如下所示
define ptr @f(i32 %n) presplitcoroutine_custom_abi {
entry:
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
%size = call i32 @llvm.coro.size.i32()
%alloc = call ptr @malloc(i32 %size)
%hdl = call noalias ptr @llvm.coro.begin.custom.abi(token %id, ptr %alloc, i32 0)
br label %loop
loop:
%n.val = phi i32 [ %n, %entry ], [ %inc, %loop ]
%inc = add nsw i32 %n.val, 1
call void @print(i32 %n.val)
%0 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %0, label %suspend [i8 0, label %loop
i8 1, label %cleanup]
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %hdl)
call void @free(ptr %mem)
br label %suspend
suspend:
%unused = call i1 @llvm.coro.end(ptr %hdl, i1 false, token none)
ret ptr %hdl
}
参数属性¶
一些参数属性(用于传达有关函数的结果或参数的附加信息)需要特殊处理。
ByVal¶
参数上的 ByVal 参数指示应将被指向者视为按值传递给函数。在协程转换之前,会生成指针的加载和存储,其中需要该值。因此,ByVal 参数的处理方式很像 alloca。在协程帧上为其分配空间,并将参数指针的用法替换为指向协程帧的指针。
Swift Error¶
Clang 在许多常见目标中支持 swiftcall 调用约定,并且用户可以从 C++ 协程调用采用 swifterror 参数的函数。 swifterror 参数属性的存在是为了建模和优化 Swift 错误处理。 swifterror alloca 或参数只能加载、存储或作为 swifterror 调用参数传递,并且 swifterror 调用参数只能是对 swifterror alloca 或参数的直接引用。这些规则并非巧合地意味着您可以始终完美地建模 alloca 中的数据流,并且 LLVM CodeGen 实际上必须这样做才能发出代码。
对于协程降低,alloca 的默认处理方式违反了这些规则 - 拆分将尝试用协程帧中的条目替换 alloca,这可能导致尝试将其作为 swifterror 参数传递。要在拆分函数中传递 swifterror 参数,我们需要仍然保留 alloca;但我们也可能需要协程帧插槽,因为有用的数据(理论上)可以跨越 presplit 协程中的挂起存储在 swifterror alloca 插槽中。当拆分协程时,因此有必要同时保留帧插槽和 alloca 本身,然后使它们保持同步。
Intrinsic¶
协程操作 Intrinsic¶
本节中描述的 Intrinsic 用于操作现有协程。它们可以在任何恰好具有指向协程帧的指针或指向协程 promise的指针的函数中使用。
‘llvm.coro.destroy’ Intrinsic¶
语法:¶
declare void @llvm.coro.destroy(ptr <handle>)
概述:¶
‘llvm.coro.destroy
’ intrinsic 销毁挂起的切换恢复协程。
参数:¶
参数是挂起协程的协程句柄。
语义:¶
如果可能,coro.destroy intrinsic 将被替换为对协程销毁函数的直接调用。否则,它将被替换为基于存储在协程帧中的销毁函数的函数指针的间接调用。销毁未挂起的协程会导致未定义的行为。
‘llvm.coro.resume’ Intrinsic¶
declare void @llvm.coro.resume(ptr <handle>)
概述:¶
‘llvm.coro.resume
’ intrinsic 恢复挂起的切换恢复协程。
参数:¶
参数是挂起协程的句柄。
语义:¶
如果可能,coro.resume intrinsic 将被替换为对协程恢复函数的直接调用。否则,它将被替换为基于存储在协程帧中的恢复函数的函数指针的间接调用。恢复未挂起的协程会导致未定义的行为。
‘llvm.coro.done’ Intrinsic¶
declare i1 @llvm.coro.done(ptr <handle>)
概述:¶
‘llvm.coro.done
’ intrinsic 检查挂起的切换恢复协程是否处于最终挂起点。
参数:¶
参数是挂起协程的句柄。
语义:¶
在没有最终挂起点的协程或未挂起的协程上使用此 intrinsic 会导致未定义的行为。
‘llvm.coro.promise’ Intrinsic¶
declare ptr @llvm.coro.promise(ptr <ptr>, i32 <alignment>, i1 <from>)
概述:¶
‘llvm.coro.promise
’ intrinsic 获取指向协程 promise的指针,给定切换恢复协程句柄,反之亦然。
参数:¶
如果 from 为 false,则第一个参数是协程的句柄。否则,它是指向协程 promise 的指针。
第二个参数是 promise 的对齐要求。如果前端将 %promise = alloca i32 指定为 promise,则 coro.promise 的对齐参数应为目标平台上 i32 的对齐方式。如果前端将 %promise = alloca i32, align 16 指定为 promise,则对齐参数应为 16。此参数仅接受常量。
第三个参数是布尔值,指示转换的方向。如果 from 为 true,则 intrinsic 返回给定指向 promise 的指针的协程句柄。如果 from 为 false,则 intrinsic 从协程句柄返回指向 promise 的指针。此参数仅接受常量。
语义:¶
在没有协程 promise 的协程上使用此 intrinsic 会导致未定义的行为。可以读取和修改当前正在执行的协程的协程 promise。协程作者和协程用户负责确保没有数据竞争。
示例:¶
define ptr @f(i32 %n) {
entry:
%promise = alloca i32
; the second argument to coro.id points to the coroutine promise.
%id = call token @llvm.coro.id(i32 0, ptr %promise, ptr null, ptr null)
...
%hdl = call noalias ptr @llvm.coro.begin(token %id, ptr %alloc)
...
store i32 42, ptr %promise ; store something into the promise
...
ret ptr %hdl
}
define i32 @main() {
entry:
%hdl = call ptr @f(i32 4) ; starts the coroutine and returns its handle
%promise.addr = call ptr @llvm.coro.promise(ptr %hdl, i32 4, i1 false)
%val = load i32, ptr %promise.addr ; load a value from the promise
call void @print(i32 %val)
call void @llvm.coro.destroy(ptr %hdl)
ret i32 0
}
协程结构 Intrinsic¶
本节中描述的 Intrinsic 在协程内使用,以描述协程结构。它们不应在协程外部使用。
‘llvm.coro.size’ Intrinsic¶
declare i32 @llvm.coro.size.i32()
declare i64 @llvm.coro.size.i64()
概述:¶
‘llvm.coro.size
’ intrinsic 返回存储协程帧所需的字节数。这仅适用于切换恢复协程。
参数:¶
无
语义:¶
coro.size intrinsic 降低为表示协程帧大小的常量。
‘llvm.coro.align’ Intrinsic¶
declare i32 @llvm.coro.align.i32()
declare i64 @llvm.coro.align.i64()
概述:¶
‘llvm.coro.align
’ intrinsic 返回协程帧的对齐方式。这仅适用于切换恢复协程。
参数:¶
无
语义:¶
coro.align intrinsic 降低为表示协程帧对齐方式的常量。
‘llvm.coro.begin’ Intrinsic¶
declare ptr @llvm.coro.begin(token <id>, ptr <mem>)
概述:¶
‘llvm.coro.begin
’ intrinsic 返回协程帧的地址。
参数:¶
第一个参数是调用 ‘llvm.coro.id
’ 返回的令牌,用于标识协程。
第二个参数是指向内存块的指针,如果动态分配协程帧,则协程帧将存储在该内存块中。此指针对于返回延续协程将被忽略。
语义:¶
根据协程帧中对象的对齐要求和/或出于 codegen 紧凑性原因,从 coro.begin 返回的指针可能与 %mem 参数存在偏移。(如果表达数据相对访问的指令可以用小的正偏移和负偏移更紧凑地编码,这将是有益的)。
前端应为每个协程发出恰好一个 coro.begin intrinsic。
‘llvm.coro.begin.custom.abi’ Intrinsic¶
declare ptr @llvm.coro.begin.custom.abi(token <id>, ptr <mem>, i32)
概述:¶
‘llvm.coro.begin.custom.abi
’ intrinsic 用来代替 coro.begin intrinsic,它有一个额外的参数来指定协程的自定义 ABI。返回值与 coro.begin intrinsic 的返回值相同。
参数:¶
第一个和第二个参数与 coro.begin intrinsic 的参数相同。
第三个参数是一个 i32 索引,指向传递给 CoroSplit 通路的生成器列表,用于指定此协程的自定义 ABI 生成器。
语义:¶
其语义与 coro.begin 内在函数相同。
‘llvm.coro.free’ 内在函数¶
declare ptr @llvm.coro.free(token %id, ptr <frame>)
概述:¶
‘llvm.coro.free
’ 内在函数返回一个指向内存块的指针,该内存块存储协程帧,如果此协程实例没有为其协程帧使用动态分配的内存,则返回 null。返回延续协程不支持此内在函数。
参数:¶
第一个参数是调用 ‘llvm.coro.id
’ 返回的令牌,用于标识协程。
第二个参数是指向协程帧的指针。这应该是由先前的 coro.begin 调用返回的同一个指针。
示例(自定义释放函数):¶
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %frame)
%mem_not_null = icmp ne ptr %mem, null
br i1 %mem_not_null, label %if.then, label %if.end
if.then:
call void @CustomFree(ptr %mem)
br label %if.end
if.end:
ret void
示例(标准释放函数):¶
cleanup:
%mem = call ptr @llvm.coro.free(token %id, ptr %frame)
call void @free(ptr %mem)
ret void
‘llvm.coro.alloc’ 内在函数¶
declare i1 @llvm.coro.alloc(token <id>)
概述:¶
‘llvm.coro.alloc
’ 内在函数返回 true,如果需要动态分配以获取协程帧的内存,否则返回 false。返回延续协程不支持此功能。
参数:¶
第一个参数是调用 ‘llvm.coro.id
’ 返回的令牌,用于标识协程。
语义:¶
前端应为每个协程最多发出一个 coro.alloc 内在函数。此内在函数用于在可能的情况下抑制协程帧的动态分配。
示例:¶
entry:
%id = call token @llvm.coro.id(i32 0, ptr null, ptr null, ptr null)
%dyn.alloc.required = call i1 @llvm.coro.alloc(token %id)
br i1 %dyn.alloc.required, label %coro.alloc, label %coro.begin
coro.alloc:
%frame.size = call i32 @llvm.coro.size()
%alloc = call ptr @MyAlloc(i32 %frame.size)
br label %coro.begin
coro.begin:
%phi = phi ptr [ null, %entry ], [ %alloc, %coro.alloc ]
%frame = call ptr @llvm.coro.begin(token %id, ptr %phi)
‘llvm.coro.noop’ 内在函数¶
declare ptr @llvm.coro.noop()
概述:¶
‘llvm.coro.noop
’ 内在函数返回一个协程帧的地址,该协程帧在恢复或销毁时不做任何事情。
参数:¶
无
语义:¶
此内在函数被降低为引用私有常量协程帧。此帧的恢复和销毁处理程序是不执行任何操作的空函数。请注意,在不同的翻译单元中,llvm.coro.noop 可能会返回不同的指针。
‘llvm.coro.frame’ 内在函数¶
declare ptr @llvm.coro.frame()
概述:¶
‘llvm.coro.frame
’ 内在函数返回封闭协程的协程帧的地址。
参数:¶
无
语义:¶
此内在函数被降低为引用 coro.begin 指令。这是一个前端便捷内在函数,使引用协程帧更容易。
‘llvm.coro.id’ 内在函数¶
declare token @llvm.coro.id(i32 <align>, ptr <promise>, ptr <coroaddr>,
ptr <fnaddrs>)
概述:¶
‘llvm.coro.id
’ 内在函数返回一个标识切换恢复协程的令牌。
参数:¶
第一个参数提供有关分配函数返回并由第一个参数提供给 coro.begin 的内存对齐方式的信息。如果此参数为 0,则假定内存与 2 * sizeof(ptr) 对齐。此参数仅接受常量。
第二个参数,如果不是 null,则指定一个特定的 alloca 指令作为 协程 promise。
第三个参数是从前端传出的 null。CoroEarly 通路将此参数设置为指向此 coro.id 所属的函数。
第四个参数在协程拆分之前是 null,之后被替换为指向一个私有全局常量数组,该数组包含指向协程的 resume 和 destroy 部分的函数指针。
语义:¶
此内在函数的目的是将属于同一协程的 coro.id、coro.alloc 和 coro.begin 捆绑在一起,以防止优化通路复制这些指令中的任何一个,除非复制了协程的整个主体。
前端应为每个协程发出一个 coro.id 内在函数。
前端应为协程发出函数属性 presplitcoroutine。
‘llvm.coro.id.async’ 内在函数¶
declare token @llvm.coro.id.async(i32 <context size>, i32 <align>,
ptr <context arg>,
ptr <async function pointer>)
概述:¶
‘llvm.coro.id.async
’ 内在函数返回一个标识异步协程的令牌。
参数:¶
第一个参数提供前端要求的 async context 的初始大小。降低过程会将帧存储所需的大小添加到此大小,并将该值存储到 async function pointer。
第二个参数是 async context 内存的对齐保证。前端保证内存将以此值对齐。
第三个参数是当前协程中的 async context 参数。
第四个参数是 async function pointer 结构的地址。降低过程将通过将协程帧大小需求添加到此内在函数的第一个参数指定的初始大小需求中,来更新此结构中的上下文大小需求。
语义:¶
前端应为每个协程发出一个 coro.id.async 内在函数。
前端应为协程发出函数属性 presplitcoroutine。
‘llvm.coro.id.retcon’ 内在函数¶
declare token @llvm.coro.id.retcon(i32 <size>, i32 <align>, ptr <buffer>,
ptr <continuation prototype>,
ptr <alloc>, ptr <dealloc>)
概述:¶
‘llvm.coro.id.retcon
’ 内在函数返回一个标识多重挂起返回延续协程的令牌。
协程的“结果类型序列”定义如下
如果协程函数的返回类型为
void
,则它是空序列;如果协程函数的返回类型为
struct
,则它是该struct
的元素类型,按顺序排列;否则,它只是协程函数的返回类型。
结果类型序列的第一个元素必须是指针类型;延续函数将被强制转换为此类型。序列的其余部分是“yield 类型”,协程中的任何挂起都必须采用这些类型的参数。
参数:¶
第一个和第二个参数是作为第三个参数提供的缓冲区的预期大小和对齐方式。它们必须是常量。
第四个参数必须是对全局函数的引用,称为“延续原型函数”。任何延续函数的类型、调用约定和属性都将从此声明中获取。原型函数的返回类型必须与当前函数的返回类型匹配。第一个参数类型必须是指针类型。第二个参数类型必须是整数类型;它将仅用作布尔标志。
第五个参数必须是对将用于分配内存的全局函数的引用。它可能不会失败,既不能返回 null 也不能抛出异常。它必须接受一个整数并返回一个指针。
第六个参数必须是对将用于释放内存的全局函数的引用。它必须接受一个指针并返回 void
。
语义:¶
前端应为协程发出函数属性 presplitcoroutine。
‘llvm.coro.id.retcon.once’ 内在函数¶
declare token @llvm.coro.id.retcon.once(i32 <size>, i32 <align>, ptr <buffer>,
ptr <prototype>,
ptr <alloc>, ptr <dealloc>)
概述:¶
‘llvm.coro.id.retcon.once
’ 内在函数返回一个标识唯一挂起返回延续协程的令牌。
参数:¶
与 llvm.core.id.retcon
相同,只是延续原型函数的返回类型必须表示延续的正常返回类型(而不是匹配协程的返回类型)。
语义:¶
前端应为协程发出函数属性 presplitcoroutine。
‘llvm.coro.end’ 内在函数¶
declare i1 @llvm.coro.end(ptr <handle>, i1 <unwind>, token <result.token>)
概述:¶
‘llvm.coro.end
’ 标记协程恢复部分的执行应结束并且控制应返回给调用者的点。
参数:¶
第一个参数应引用封闭协程的协程句柄。前端允许提供 null 作为第一个参数,在这种情况下,coro-early 通路将用适当的协程句柄值替换 null。
第二个参数应该是 true,如果此 coro.end 位于由于异常而离开协程主体的展开序列的一部分的块中,否则为 false。
非平凡(非none)令牌参数只能为唯一挂起返回延续协程指定,其中它必须是由 ‘llvm.coro.end.results
’ 内在函数生成的令牌值。
在展开部分中,只允许使用 none 令牌进行 coro.end 调用
语义:¶
此内在函数的目的是允许前端标记清理和其他仅在协程的初始调用期间相关的代码,并且不应出现在恢复和销毁部分中。
在返回延续降低中,llvm.coro.end
完全销毁协程帧。如果第二个参数为 false,它也会从协程返回 null 延续指针,并且下一条指令将无法访问。如果第二个参数为 true,它会向下传递,以便后续逻辑可以继续展开。在 yield-once 协程中,在没有首先到达 llvm.coro.suspend.retcon
的情况下到达非展开 llvm.coro.end
具有未定义的行为。
本节的其余部分描述了在切换恢复降低下的行为。
当协程被拆分为启动、恢复和销毁部分时,此内在函数会被降低。在启动部分中,它是一个空操作,在恢复和销毁部分中,它被替换为 ret void 指令,并且包含 coro.end 指令的块的其余部分被丢弃。在着陆区中,它被替换为适当的指令以展开到调用者。coro.end 的处理方式因目标是否使用 landingpad 或 WinEH 异常模型而异。
对于基于 landingpad 的异常模型,预期前端按如下方式使用 coro.end 内在函数
ehcleanup:
%InResumePart = call i1 @llvm.coro.end(ptr null, i1 true, token none)
br i1 %InResumePart, label %eh.resume, label %cleanup.cont
cleanup.cont:
; rest of the cleanup
eh.resume:
%exn = load ptr, ptr %exn.slot, align 8
%sel = load i32, ptr %ehselector.slot, align 4
%lpad.val = insertvalue { ptr, i32 } undef, ptr %exn, 0
%lpad.val29 = insertvalue { ptr, i32 } %lpad.val, i32 %sel, 1
resume { ptr, i32 } %lpad.val29
CoroSpit 通路在恢复函数中将 coro.end 替换为 True
,从而导致立即展开到调用者,而在启动函数中,它被替换为 False
,从而允许继续执行仅在协程的初始调用期间需要的其余清理代码。
对于 Windows 异常处理模型,前端应附加一个 funclet 捆绑包,该捆绑包引用封闭的 cleanuppad,如下所示
ehcleanup:
%tok = cleanuppad within none []
%unused = call i1 @llvm.coro.end(ptr null, i1 true, token none) [ "funclet"(token %tok) ]
cleanupret from %tok unwind label %RestOfTheCleanup
CoroSplit 通路,如果存在 funclet 捆绑包,将在 coro.end 内在函数之前插入 cleanupret from %tok unwind to caller
,并将删除块的其余部分。
在展开路径中(当参数为 true 时),coro.end 将协程标记为完成,使得再次恢复协程的行为未定义,并导致 llvm.coro.done 返回 true。这在正常路径中不是必需的,因为协程将已经通过最终挂起标记为完成。
下表总结了 coro.end 内在函数的处理方式。
在启动函数中 |
在恢复/销毁函数中 |
||
unwind=false |
无 |
|
|
unwind=true |
WinEH |
将协程标记为完成 |
cleanupret unwind to caller 将协程标记为完成
|
Landingpad |
将协程标记为完成 |
将协程标记为完成 |
‘llvm.coro.end.results’ 内在函数¶
declare token @llvm.coro.end.results(...)
概述:¶
‘llvm.coro.end.results
’ 内在函数捕获要从唯一挂起返回延续协程返回的值。
参数:¶
参数的数量必须与延续函数的返回类型匹配
如果延续函数的返回类型为
void
,则必须没有参数如果延续函数的返回类型为
struct
,则参数将是该struct
的元素类型,按顺序排列;否则,它只是延续函数的返回值。
define {ptr, ptr} @g(ptr %buffer, ptr %ptr, i8 %val) presplitcoroutine {
entry:
%id = call token @llvm.coro.id.retcon.once(i32 8, i32 8, ptr %buffer,
ptr @prototype,
ptr @allocate, ptr @deallocate)
%hdl = call ptr @llvm.coro.begin(token %id, ptr null)
...
cleanup:
%tok = call token (...) @llvm.coro.end.results(i8 %val)
call i1 @llvm.coro.end(ptr %hdl, i1 0, token %tok)
unreachable
...
declare i8 @prototype(ptr, i1 zeroext)
‘llvm.coro.end.async’ 内在函数¶
declare i1 @llvm.coro.end.async(ptr <handle>, i1 <unwind>, ...)
概述:¶
‘llvm.coro.end.async
’ 标记协程恢复部分的执行应结束并且控制应返回给调用者的点。作为其可变尾部参数的一部分,此指令允许指定一个函数和要作为返回之前的最后一个操作进行尾部调用的函数的参数。
参数:¶
第一个参数应引用封闭协程的协程句柄。前端允许提供 null 作为第一个参数,在这种情况下,coro-early 通路将用适当的协程句柄值替换 null。
第二个参数应该是 true,如果此 coro.end 位于由于异常而离开协程主体的展开序列的一部分的块中,否则为 false。
第三个参数(如果存在)应指定要调用的函数。
如果存在第三个参数,则其余参数是函数调用的参数。
call i1 (ptr, i1, ...) @llvm.coro.end.async(
ptr %hdl, i1 0,
ptr @must_tail_call_return,
ptr %ctxt, ptr %task, ptr %actor)
unreachable
‘llvm.coro.suspend’ 内在函数¶
declare i8 @llvm.coro.suspend(token <save>, i1 <final>)
概述:¶
‘llvm.coro.suspend
’ 标记切换恢复协程的执行被挂起并且控制返回给调用者的点。消耗此内在函数结果的条件分支导致协程应在挂起 (-1)、恢复 (0) 或销毁 (1) 时继续执行的基本块。
参数:¶
第一个参数引用 coro.save 内在函数的令牌,该令牌标记协程状态准备好挂起的点。如果传递 none 令牌,则内在函数的行为就好像在 coro.suspend 内在函数之前立即存在 coro.save 一样。
第二个参数指示此挂起点是否是最终的。第二个参数仅接受常量。如果多个挂起点被指定为最终挂起点,则恢复和销毁分支应导致相同的基本块。
示例(正常挂起点):¶
%0 = call i8 @llvm.coro.suspend(token none, i1 false)
switch i8 %0, label %suspend [i8 0, label %resume
i8 1, label %cleanup]
示例(最终挂起点):¶
while.end:
%s.final = call i8 @llvm.coro.suspend(token none, i1 true)
switch i8 %s.final, label %suspend [i8 0, label %trap
i8 1, label %cleanup]
trap:
call void @llvm.trap()
unreachable
语义:¶
如果通过 coro.resume 恢复在此内在函数标记的挂起点处挂起的协程,则控制将转移到 0-case 的基本块。如果通过 coro.destroy 恢复,它将继续执行 1-case 指示的基本块。要挂起,协程继续执行默认标签。
如果挂起内在函数被标记为最终挂起,它可以认为 true 分支不可访问,并且可以执行可以利用该事实的优化。
‘llvm.coro.save’ 内在函数¶
declare token @llvm.coro.save(ptr <handle>)
概述:¶
‘llvm.coro.save
’ 标记协程需要更新其状态以准备恢复以被视为挂起的点(因此有资格恢复)。除非它们的 ‘llvm.coro.suspend
’ 用户也被合并,否则合并两个 ‘llvm.coro.save
’ 调用是非法的。因此,‘llvm.coro.save
’ 当前标记有 no_merge 函数属性。
参数:¶
第一个参数指向封闭协程的协程句柄。
语义:¶
启用从相应挂起点恢复协程所需的任何协程状态更改都应在 coro.save 内在函数的点处完成。
示例:¶
当协程用于表示由表示异步操作完成的回调驱动的异步控制流时,单独的 save 和 suspend 点是必要的。
在这种情况下,协程应该在调用 async_op 函数之前准备好恢复,该函数可能会在 async_op 调用将控制权返回给协程之前,从同一线程或不同线程触发协程的恢复
%save1 = call token @llvm.coro.save(ptr %hdl)
call void @async_op1(ptr %hdl)
%suspend1 = call i1 @llvm.coro.suspend(token %save1, i1 false)
switch i8 %suspend1, label %suspend [i8 0, label %resume1
i8 1, label %cleanup]
‘llvm.coro.suspend.async’ 内在函数¶
declare {ptr, ptr, ptr} @llvm.coro.suspend.async(
ptr <resume function>,
ptr <context projection function>,
... <function to call>
... <arguments to function>)
概述:¶
‘llvm.coro.suspend.async
’ 内在函数标记异步协程的执行被挂起并且控制权传递给被调用者的点。
参数:¶
第一个参数应该是 llvm.coro.async.resume 内在函数的结果。降低过程将用此挂起点的恢复函数替换此内在函数。
第二个参数是 context projection function。它应描述如何从延续函数的第一个参数中恢复延续函数中的 async context。其类型为 ptr (ptr)。
第三个参数是模拟在挂起点转移到被调用者的函数。它应接受 3 个参数。降低过程将 musttail 调用此函数。
第四到第六个参数是第三个参数的参数。
语义:¶
内在函数的结果映射到恢复函数的参数。执行在此内在函数处挂起,并在调用恢复函数时恢复。
‘llvm.coro.prepare.async’ 内在函数¶
declare ptr @llvm.coro.prepare.async(ptr <coroutine function>)
概述:¶
‘llvm.coro.prepare.async
’ 内在函数用于阻止异步协程的内联,直到协程拆分之后。
参数:¶
第一个参数应该是类型为 void (ptr, ptr, ptr) 的异步协程。降低过程将用其协程函数参数替换此内在函数。
‘llvm.coro.suspend.retcon’ 内在函数¶
declare i1 @llvm.coro.suspend.retcon(...)
概述:¶
‘llvm.coro.suspend.retcon
’ 内在函数标记返回延续协程的执行被挂起并且控制返回给调用者的点。
llvm.coro.suspend.retcon` 不支持单独的 save 点;当延续函数不可本地访问时,它们没有用处。这对于尚未实现的 passcon
降低来说将是一个更合适的功能。
参数:¶
参数的类型必须与协程的 yielded-types 序列完全匹配。它们将与下一个延续函数一起转换为 ramp 和延续函数的返回值。
语义:¶
内在函数的结果指示协程是否应异常恢复(非零)。
在正常协程中,如果协程在异常恢复后执行对 llvm.coro.suspend.retcon
的调用,则行为未定义。
在 yield-once 协程中,如果协程在以任何方式恢复后执行对 llvm.coro.suspend.retcon
的调用,则行为未定义。
‘llvm.coro.await.suspend.void’ 内在函数¶
declare void @llvm.coro.await.suspend.void(
ptr <awaiter>,
ptr <handle>,
ptr <await_suspend_function>)
概述:¶
‘llvm.coro.await.suspend.void
’ 内在函数封装 C++ await-suspend 块,直到它不会干扰协程转换。
co_await 的 await_suspend 块本质上与协程的执行异步。通常将其内联到未拆分的协程中可能会导致错误编译,因为协程 CFG 错误地表示了程序的真实控制流:await_suspend 中发生的事情不能保证在协程恢复之前发生,并且在协程恢复之后发生的事情(包括其退出和协程帧的潜在释放)不能保证仅在 await_suspend 结束后发生。
此版本的内在函数对应于 ‘void awaiter.await_suspend(...)
’ 变体。
参数:¶
第一个参数是指向 awaiter 对象的指针。
第二个参数是指向当前协程帧的指针。
第三个参数是指向封装 await-suspend 逻辑的包装器函数的指针。其签名必须是
declare void @await_suspend_function(ptr %awaiter, ptr %hdl)
语义:¶
内在函数必须在对应的 coro.save 和 coro.suspend 调用之间使用。在 CoroSplit 通路期间,它被降低为直接 await_suspend_function 调用。
示例:¶
; before lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
call void @llvm.coro.await.suspend.void(
ptr %awaiter,
ptr %hdl,
ptr @await_suspend_function)
%suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
...
; after lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
; the call to await_suspend_function can be inlined
call void @await_suspend_function(
ptr %awaiter,
ptr %hdl)
%suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
...
; wrapper function example
define void @await_suspend_function(ptr %awaiter, ptr %hdl)
entry:
%hdl.arg = ... ; construct std::coroutine_handle from %hdl
call void @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
ret void
‘llvm.coro.await.suspend.bool’ 内在函数¶
declare i1 @llvm.coro.await.suspend.bool(
ptr <awaiter>,
ptr <handle>,
ptr <await_suspend_function>)
概述:¶
‘llvm.coro.await.suspend.bool
’ 内在函数封装 C++ await-suspend 块,直到它不会干扰协程转换。
co_await 的 await_suspend 块本质上与协程的执行异步。通常将其内联到未拆分的协程中可能会导致错误编译,因为协程 CFG 错误地表示了程序的真实控制流:await_suspend 中发生的事情不能保证在协程恢复之前发生,并且在协程恢复之后发生的事情(包括其退出和协程帧的潜在释放)不能保证仅在 await_suspend 结束后发生。
此版本的内在函数对应于 ‘bool awaiter.await_suspend(...)
’ 变体。
参数:¶
第一个参数是指向 awaiter 对象的指针。
第二个参数是指向当前协程帧的指针。
第三个参数是指向封装 await-suspend 逻辑的包装器函数的指针。其签名必须是
declare i1 @await_suspend_function(ptr %awaiter, ptr %hdl)
语义:¶
内在函数必须在对应的 coro.save 和 coro.suspend 调用之间使用。在 CoroSplit 通路期间,它被降低为直接 await_suspend_function 调用。
如果 await_suspend_function 调用返回 true,则当前协程将立即恢复。
示例:¶
; before lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
%resume = call i1 @llvm.coro.await.suspend.bool(
ptr %awaiter,
ptr %hdl,
ptr @await_suspend_function)
br i1 %resume, %await.suspend.bool, %await.ready
await.suspend.bool:
%suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
...
await.ready:
call void @"Awaiter::await_resume"(ptr %awaiter)
...
; after lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
; the call to await_suspend_function can inlined
%resume = call i1 @await_suspend_function(
ptr %awaiter,
ptr %hdl)
br i1 %resume, %await.suspend.bool, %await.ready
...
; wrapper function example
define i1 @await_suspend_function(ptr %awaiter, ptr %hdl)
entry:
%hdl.arg = ... ; construct std::coroutine_handle from %hdl
%resume = call i1 @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
ret i1 %resume
‘llvm.coro.await.suspend.handle’ 内在函数¶
declare void @llvm.coro.await.suspend.handle(
ptr <awaiter>,
ptr <handle>,
ptr <await_suspend_function>)
概述:¶
‘llvm.coro.await.suspend.handle
’ 内在函数封装 C++ await-suspend 块,直到它不会干扰协程转换。
co_await 的 await_suspend 块本质上与协程的执行异步。通常将其内联到未拆分的协程中可能会导致错误编译,因为协程 CFG 错误地表示了程序的真实控制流:await_suspend 中发生的事情不能保证在协程恢复之前发生,并且在协程恢复之后发生的事情(包括其退出和协程帧的潜在释放)不能保证仅在 await_suspend 结束后发生。
此版本的内在函数对应于 ‘std::corouine_handle<> awaiter.await_suspend(...)
’ 变体。
参数:¶
第一个参数是指向 awaiter 对象的指针。
第二个参数是指向当前协程帧的指针。
第三个参数是指向封装 await-suspend 逻辑的包装器函数的指针。其签名必须是
declare ptr @await_suspend_function(ptr %awaiter, ptr %hdl)
语义:¶
内在函数必须在对应的 coro.save 和 coro.suspend 调用之间使用。在 CoroSplit 通路期间,它被降低为直接 await_suspend_function 调用。
await_suspend_function 必须返回指向有效协程帧的指针。内在函数将被降低为尾部调用,恢复返回的协程帧。在支持尾部调用的目标上,它将被标记为 musttail。内在函数之后的指令将变为不可访问。
示例:¶
; before lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
call void @llvm.coro.await.suspend.handle(
ptr %awaiter,
ptr %hdl,
ptr @await_suspend_function)
%suspend = call i8 @llvm.coro.suspend(token %save, i1 false)
...
; after lowering
await.suspend:
%save = call token @llvm.coro.save(ptr %hdl)
; the call to await_suspend_function can be inlined
%next = call ptr @await_suspend_function(
ptr %awaiter,
ptr %hdl)
musttail call void @llvm.coro.resume(%next)
ret void
...
; wrapper function example
define ptr @await_suspend_function(ptr %awaiter, ptr %hdl)
entry:
%hdl.arg = ... ; construct std::coroutine_handle from %hdl
%hdl.raw = call ptr @"Awaiter::await_suspend"(ptr %awaiter, ptr %hdl.arg)
%hdl.result = ... ; get address of returned coroutine handle
ret ptr %hdl.result
协程转换通路¶
CoroEarly¶
CoroEarly 通路降低了隐藏协程帧结构细节的协程内在函数,但对于帮助后续协程通路来说,其他方面不是必须保留的。此通路降低了 coro.frame、coro.done 和 coro.promise 内在函数。
CoroSplit¶
CoroSplit 通路构建协程帧并将恢复和销毁部分外联到单独的函数中。此通路还降低了 coro.await.suspend.void、coro.await.suspend.bool 和 coro.await.suspend.handle 内在函数。
CoroAnnotationElide¶
此通路查找所有“必须省略”的协程用法,并将 coro.begin 内在函数替换为放置在其调用者上的协程帧的地址,并将 coro.alloc 和 coro.free 内在函数分别替换为 false 和 null,以删除释放代码。
CoroElide¶
CoroElide 通路检查内联协程是否符合堆分配省略优化条件。如果是,它会将 coro.begin 内在函数替换为放置在其调用者上的协程帧的地址,并将 coro.alloc 和 coro.free 内在函数分别替换为 false 和 null,以删除释放代码。此通路还会在可能的情况下,将 coro.resume 和 coro.destroy 内在函数替换为对特定协程的恢复和销毁函数的直接调用。
CoroCleanup¶
此通路在后期运行,以降低所有未被早期通路替换的协程相关内在函数。
属性¶
coro_only_destroy_when_complete¶
当协程被标记为 coro_only_destroy_when_complete 时,它表示协程必须在销毁时到达最终挂起点。
此属性目前仅适用于切换恢复协程。
coro_elide_safe¶
当对切换 ABI 协程 f 的 Call 或 Invoke 指令被标记为 coro_elide_safe 时,CoroSplitPass 会生成一个 f.noalloc ramp 函数。f.noalloc 比其原始 ramp 函数 f 多一个参数,即指向已分配帧的指针。f.noalloc 还会抑制任何可能受 @llvm.coro.alloc 和 @llvm.coro.free 保护的分配或释放。
CoroAnnotationElidePass 在可能的情况下执行堆 elision。请注意,对于递归或相互递归函数,通常不可能进行此 elision。
元数据¶
‘coro.outside.frame
’ 元数据¶
coro.outside.frame
元数据可以附加到 alloca 指令,以表示不应将其提升到协程帧,这对于前端在发出内部控制机制时过滤掉 allocas 非常有用。此外,此元数据仅用作标志,因此关联节点必须为空。
%__coro_gro = alloca %struct.GroType, align 1, !coro.outside.frame !0
...
!0 = !{}
需要注意的区域¶
当 coro.suspend 返回 -1 时,协程被挂起,并且协程可能已被销毁(因此帧已被释放)。我们无法访问挂起路径上的帧上的任何内容。但是,没有什么可以阻止编译器沿该路径移动指令(例如 LICM),这可能导致 use-after-free。目前,我们禁用了具有 coro.suspend 的循环的 LICM,但一般问题仍然存在,需要一个通用的解决方案。
利用生命周期内建函数来处理进入协程帧的数据。对于保留在 allocas 中的数据,保持生命周期内建函数不变。
CoroElide 优化 pass 依赖于协程 ramp 函数被内联。进一步拆分 ramp 函数以增加其被内联到其调用者的机会将是有益的。
设计一种约定,使其可以跨 ABI 边界应用协程堆 elision 优化。
无法处理带有 inalloca 参数的协程(在 Windows 上的 x86 中使用)。
coro.begin 和 coro.free 内建函数忽略对齐。
进行必要的更改,以确保协程优化与 LTO 一起工作。
更多测试,更多测试,更多测试