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 函数,该函数调用协程内联函数来定义协程的结构。然后,在最一般的情况下,协程降低过程会重写协程函数,使其成为“斜坡函数”(协程的初始入口点),该函数执行直到第一次到达挂起点。原始协程函数的其余部分被拆分为多个“恢复函数”。必须跨挂起保持的任何状态都存储在协程帧中。恢复函数必须能够以某种方式处理“正常”恢复(继续协程的正常执行)或“异常”恢复(必须在不尝试挂起协程的情况下展开协程)。
切换恢复降低¶
在 LLVM 的标准切换恢复降低中,通过使用llvm.coro.id来表示,协程帧存储为“协程对象”的一部分,该对象表示协程特定调用的句柄。所有协程对象都支持通用 ABI,允许在不知道协程实现的任何信息的情况下使用某些功能。
可以使用llvm.coro.done查询协程对象以查看它是否已完成。
如果协程对象尚未完成,则可以使用llvm.coro.resume正常恢复它。
可以使用llvm.coro.destroy销毁协程对象,使协程对象无效。即使协程已正常完成,也必须单独执行此操作。
可以使用llvm.coro.promise从协程对象中投影出“Promise”存储,已知该存储具有特定的大小和对齐方式。协程实现必须已编译为定义相同大小和对齐方式的 Promise。
通常,在协程运行时以任何方式与协程对象交互的行为都是未定义的。
协程函数被拆分为三个函数,表示控制进入协程的三种不同方式。
最初调用的斜坡函数,它接受任意参数并返回指向协程对象的指针;
恢复协程时调用的协程恢复函数,它接受指向协程对象的指针并返回void;
销毁协程时调用的协程销毁函数,它接受指向协程对象的指针并返回void。
由于恢复和销毁函数在所有挂起点之间共享,因此挂起点必须在协程对象中存储活动挂起的索引,并且恢复/销毁函数必须切换该索引才能返回到正确的点。因此得名这种降低方式。
指向恢复和销毁函数的指针存储在协程对象中已知的偏移量处,这些偏移量对于所有协程都是固定的。已完成的协程由空恢复函数表示。
分配和释放协程对象有一个稍微复杂的内联函数协议。它很复杂,以便允许由于内联而省略分配。下面将更详细地讨论此协议。
前端可能会生成代码以直接调用协程函数;这将成为对斜坡函数的调用,并将返回指向协程对象的指针。前端应始终使用相应的内联函数恢复或销毁协程。
返回延续降低¶
在返回延续降低中,通过使用llvm.coro.id.retcon或llvm.coro.id.retcon.once来表示,前端必须更明确地处理 ABI 的某些方面。
在此降低中,每个挂起点都带有一系列“生成的返回值”,这些值与函数指针(称为延续函数)一起返回给调用方。通过简单地调用此延续函数指针来恢复协程。原始协程被划分为斜坡函数,然后是任意数量的这些延续函数,每个挂起点一个。
LLVM 实际上支持两种密切相关的返回延续降低方式。
在正常的返回延续降低中,协程可以多次挂起自身。这意味着延续函数本身返回另一个延续指针,以及一系列生成的返回值。
协程通过返回空延续指针来指示它已运行完成。任何生成的返回值都将是undef,应被忽略。
在一次生成返回延续降低中,协程必须精确挂起一次(或抛出异常)。斜坡函数返回延续函数指针和生成的返回值,延续函数在协程运行完成时可以选择返回普通结果。
协程帧维护在一个固定大小的缓冲区中,该缓冲区传递给coro.id 内联函数,该内联函数静态保证了特定的大小和对齐方式。必须将相同的缓冲区传递给延续函数。如果缓冲区不足,协程将分配内存,在这种情况下,它需要至少将该指针存储在缓冲区中;因此,缓冲区必须始终至少为指针大小。协程如何使用缓冲区可能在挂起点之间有所不同。
除了缓冲区指针之外,延续函数还采用一个参数来指示协程是正常恢复(零)还是异常恢复(非零)。
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内联函数表示。通过调用此恢复函数并将异步上下文作为其参数之一传递来恢复协程。 恢复函数可以通过应用上下文投影函数来恢复其(调用方)的异步上下文,该函数由前端作为参数提供给llvm.coro.suspend.async内联函数。
// 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内联函数。降低将使用协程帧需求更新大小条目。前端负责为异步上下文分配内存,但可以使用异步函数指针结构获取所需的大小。
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 内联函数被降低为一个常数,表示协程框架所需的大小。 coro.begin 内联函数初始化协程框架并返回协程句柄。 coro.begin 的第二个参数被赋予一个内存块,如果协程框架需要动态分配,则使用该内存块。 coro.id 内联函数用作协程标识,在优化传递(如跳转线程)复制 coro.begin 内联函数的情况下很有用。
cleanup 块销毁协程框架。 coro.free 内联函数在给定协程句柄的情况下,返回要释放的内存块的指针,或者如果协程框架不是动态分配的,则返回 null。当协程自行运行到完成或通过调用 coro.destroy 内联函数销毁时,就会进入 cleanup 块。
suspend 块包含协程运行到完成或挂起时要执行的代码。 coro.end 内联函数标记协程需要将控制权返回给调用者的位置,前提是它不是协程的初始调用。
loop 块表示协程的主体。 coro.suspend 内联函数与以下 switch 结合使用,指示协程挂起(默认情况)、恢复(情况 0)或销毁(情况 1)时控制流会发生什么。
协程转换¶
协程降低的步骤之一是构建协程框架。分析 def-use 链以确定哪些对象需要在挂起点之间保持活动状态。在上一节中显示的协程中,虚拟寄存器 %inc 的使用与定义之间存在挂起点,因此它不能驻留在栈帧上,因为一旦协程挂起并将控制权返回给调用者,栈帧就会消失。在协程框架中分配一个 i32 槽,并根据需要将 %inc 溢出并从该槽中重新加载。
我们还存储恢复和销毁函数的地址,以便 coro.resume 和 coro.destroy 内联函数可以在编译时无法静态确定协程标识的情况下恢复和销毁协程。对于我们的示例,协程框架将是
%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 来避免动态分配。
在入口块中,我们将调用 coro.alloc 内联函数,该内联函数将在需要动态分配时返回 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)
在清理块中,我们将使释放协程框架的操作取决于 coro.free 内联函数。如果省略了分配,则 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 内联函数用于指示协程应准备好恢复的点(即,何时应将恢复索引存储在协程框架中,以便它可以在正确的恢复点恢复)
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]
协程承诺¶
协程作者或前端可以指定一个特殊的 alloca,用于与协程通信。此特殊的 alloca 称为 **协程承诺**,并作为 coro.id 内联函数的第二个参数提供。
以下协程指定了一个 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 内联函数来访问协程承诺。
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 内联函数的第二个参数设置为 true 来指定特定的挂起为最终挂起。此类挂起点具有两个属性
可以通过 coro.done 内联函数检查挂起的协程是否位于最终挂起点;
恢复在最终挂起点停止的协程会导致未定义的行为。协程在最终挂起点唯一可能的操作是通过 coro.destroy 内联函数销毁它。
从用户的角度来看,最终挂起点表示协程到达结束的概念。从编译器的角度来看,它是减少恢复点(因此减少恢复函数中的 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);
}
内联函数¶
协程操作内联函数¶
本节中描述的内联函数用于操作现有的协程。它们可以在任何碰巧拥有指向 协程框架 或指向 协程承诺 的指针的函数中使用。
‘llvm.coro.destroy’ 内联函数¶
语法:¶
declare void @llvm.coro.destroy(ptr <handle>)
概述:¶
‘llvm.coro.destroy
’ 内联函数销毁挂起的切换-恢复协程。
参数:¶
参数是挂起协程的协程句柄。
语义:¶
如果可能,coro.destroy 内联函数将替换为对协程销毁函数的直接调用。否则,它将替换为基于存储在协程框架中的销毁函数的函数指针的间接调用。销毁未挂起的协程会导致未定义的行为。
‘llvm.coro.resume’ 内联函数¶
declare void @llvm.coro.resume(ptr <handle>)
概述:¶
‘llvm.coro.resume
’ 内联函数恢复挂起的切换-恢复协程。
参数:¶
参数是挂起协程的句柄。
语义:¶
如果可能,coro.resume 内联函数将替换为对协程恢复函数的直接调用。否则,它将替换为基于存储在协程框架中的恢复函数的函数指针的间接调用。恢复未挂起的协程会导致未定义的行为。
‘llvm.coro.done’ 内联函数¶
declare i1 @llvm.coro.done(ptr <handle>)
概述:¶
‘llvm.coro.done
’ 内联函数检查挂起的切换-恢复协程是否位于最终挂起点。
参数:¶
参数是挂起协程的句柄。
语义:¶
在没有 最终挂起 点或未挂起的协程上使用此内联函数会导致未定义的行为。
‘llvm.coro.promise’ 内联函数¶
declare ptr @llvm.coro.promise(ptr <ptr>, i32 <alignment>, i1 <from>)
概述:¶
‘llvm.coro.promise
’ 内联函数根据切换恢复协程句柄获取指向 协程承诺 的指针,反之亦然。
参数:¶
如果 from 为假,则第一个参数是协程的句柄。否则,它是指向协程承诺的指针。
第二个参数是承诺的对齐要求。如果前端将 %promise = alloca i32 指定为承诺,则 coro.promise 的对齐参数应为目标平台上 i32 的对齐方式。如果前端将 %promise = alloca i32, align 16 指定为承诺,则对齐参数应为 16。此参数仅接受常量。
第三个参数是指示转换方向的布尔值。如果 from 为真,则内联函数根据指向承诺的指针返回协程句柄。如果 from 为假,则内联函数根据协程句柄返回指向承诺的指针。此参数仅接受常量。
语义:¶
在没有协程承诺的协程上使用此内联函数会导致未定义的行为。可以读取和修改当前正在执行的协程的协程承诺。协程作者和协程使用者有责任确保没有数据竞争。
示例:¶
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
}
协程结构内联函数¶
本节中描述的内联函数用于协程内描述协程结构。它们不应在协程外部使用。
‘llvm.coro.size’ 内联函数¶
declare i32 @llvm.coro.size.i32()
declare i64 @llvm.coro.size.i64()
概述:¶
‘llvm.coro.size
’ 内联函数返回存储 协程框架 所需的字节数。这仅支持切换恢复协程。
参数:¶
无
语义:¶
coro.size 内联函数被降低为表示协程框架大小的常量。
‘llvm.coro.align’ 内联函数¶
declare i32 @llvm.coro.align.i32()
declare i64 @llvm.coro.align.i64()
概述:¶
‘llvm.coro.align
’ 内联函数返回 协程框架 的对齐方式。这仅支持切换恢复协程。
参数:¶
无
语义:¶
coro.align 内联函数被降低为表示协程框架对齐方式的常量。
‘llvm.coro.begin’ 内联函数¶
declare ptr @llvm.coro.begin(token <id>, ptr <mem>)
概述:¶
‘llvm.coro.begin
’ 内联函数返回协程框架的地址。
参数:¶
第一个参数是通过调用 ‘llvm.coro.id
’ 返回的标记,用于标识协程。
第二个参数是指向内存块的指针,如果协程框架是动态分配的,则将存储协程框架。对于返回延续协程,此指针将被忽略。
语义:¶
根据协程框架中对象的对齐要求和/或代码生成紧凑性原因,从 coro.begin 返回的指针可能位于 %mem 参数的偏移处。(如果表示数据相对访问的指令可以用小的正负偏移量更紧凑地编码,这将是有益的)。
前端应为每个协程发出正好一个 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 指令作为 协程承诺。
第三个参数是来自前端的 null。CoroEarly 通道将此参数设置为指向此 coro.id 所属的函数。
第四个参数在协程拆分之前为 null,之后替换为指向包含指向协程的轮廓恢复和销毁部分的函数指针的私有全局常量数组。
语义:¶
此内联函数的目的是将属于同一协程的 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
’ 内联函数返回一个标记,用于标识异步协程。
参数:¶
第一个参数提供前端所需的 异步上下文 的初始大小。降低将为此大小添加框架存储所需的大小,并将该值存储到 异步函数指针 中。
第二个参数是 异步上下文 内存的对齐保证。前端保证内存将以此值为对齐。
第三个参数是当前协程中的 异步上下文 参数。
第四个参数是 异步函数指针 结构的地址。降低将通过将协程框架大小需求添加到此内联函数的第一个参数指定 的初始大小需求来更新此结构中的上下文大小需求。
语义:¶
前端应为每个协程发出正好一个 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 类型”,协程中的任何挂起都必须采用这些类型的参数。
参数:¶
第一个和第二个参数是作为第三个参数提供的缓冲区的预期大小和对齐方式。它们必须是常量。
第四个参数必须是对全局函数的引用,称为“继续原型函数”。任何继续函数的类型、调用约定和属性都将从此声明中获取。原型函数的返回类型必须与当前函数的返回类型匹配。第一个参数类型必须是指针类型。第二个参数类型必须是整数类型;它将仅用作布尔标志。
第五个参数必须是对全局函数的引用,该函数将用于分配内存。它不能失败,无论是通过返回空值还是抛出异常。它必须接受一个整数并返回一个指针。
第六个参数必须是对全局函数的引用,该函数将用于释放内存。它必须接受一个指针并返回 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
” 标记协程恢复部分执行应结束且控制应返回调用方的点。
参数:¶
第一个参数应引用封闭协程的协程句柄。前端允许提供空值作为第一个参数,在这种情况下,coro-early 传递将用适当的协程句柄值替换空值。
如果此 coro.end 位于由于异常而离开协程主体展开序列的一部分的块中,则第二个参数应为 true,否则为 false。
非平凡(非 none)标记参数只能在唯一挂起返回继续协程中指定,其中它必须是“llvm.coro.end.results
” 内联函数生成的标记值。
在展开部分的 coro.end 调用中,仅允许 none 标记。
语义:¶
此内联函数的目的是允许前端标记仅在协程的初始调用期间相关的清理和其他代码,并且不应出现在恢复和销毁部分中。
在返回继续降低中,“llvm.coro.end
” 完全销毁协程框架。如果第二个参数为 false,它还会使用空继续指针从协程返回,并且下一条指令将不可访问。如果第二个参数为 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 异常处理模型,前端应附加一个引用封闭清理垫的函数块,如下所示
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 传递,如果存在函数块,将在 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
” 标记协程恢复部分执行应结束且控制应返回调用方的点。作为其可变尾部参数的一部分,此指令允许指定一个函数和该函数的参数,这些参数将在返回之前作为最后一个操作进行尾调用。
参数:¶
第一个参数应引用封闭协程的协程句柄。前端允许提供空值作为第一个参数,在这种情况下,coro-early 传递将用适当的协程句柄值替换空值。
如果此 coro.end 位于由于异常而离开协程主体展开序列的一部分的块中,则第二个参数应为 true,否则为 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 案例的基本块。如果通过 coro.destroy 恢复它,它将继续执行 1 案例指示的基本块。要挂起,协程将继续执行默认标签。
如果挂起内联函数被标记为最终,则它可以认为 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 内联函数的点上,应该完成任何使协程能够从相应的挂起点恢复所需的协程状态更改。
示例:¶
当协程用于表示由表示异步操作完成的回调驱动的异步控制流时,需要单独的保存和挂起点。
在这种情况下,协程应该在调用可能触发从相同或不同线程恢复协程的 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 内联函数的结果。降低将用此挂起点的恢复函数替换此内联函数。
第二个参数是 上下文投影函数。它应该描述如何在继续函数的第一个参数中的继续函数中恢复 异步上下文。其类型为 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` 不支持单独的保存点;当继续函数在本地不可访问时,它们没有用处。这将是尚未实现的 passcon
降低的更合适的特性。
参数:¶
参数的类型必须与协程的生成类型序列完全匹配。它们将与下一个继续函数一起变成斜坡和继续函数的返回值。
语义:¶
内联函数的结果指示协程是否应该异常恢复(非零)。
在普通协程中,如果协程在异常恢复后执行对 llvm.coro.suspend.retcon
的调用,则行为未定义。
在一次生成协程中,如果协程以任何方式恢复后执行对 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 内联函数。
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.outside.frame
’ 元数据¶
可能将coro.outside.frame
元数据附加到 alloca 指令,以表示它不应该提升到协程框架中,这对于前端在发出内部控制机制时过滤 alloca很有用。此外,此元数据仅用作标志,因此关联的节点必须为空。
%__coro_gro = alloca %struct.GroType, align 1, !coro.outside.frame !0
...
!0 = !{}
需要关注的领域¶
当 coro.suspend 返回 -1 时,协程被挂起,并且协程可能已经被销毁(因此框架已被释放)。我们无法在挂起路径上的框架上访问任何内容。但是,没有任何内容可以阻止编译器沿该路径移动指令(例如 LICM),这可能导致使用后释放。目前,我们禁用了具有 coro.suspend 的循环的 LICM,但总体问题仍然存在,需要一个通用的解决方案。
利用进入协程框架的数据的生命周期内在函数。对于保留在 alloca 中的数据,保持生命周期内在函数不变。
CoroElide 优化过程依赖于协程斜坡函数被内联。进一步拆分斜坡函数将有利于增加它被内联到其调用者中的可能性。
设计一种约定,使跨 ABI 边界应用协程堆省略优化成为可能。
无法处理带有inalloca参数的协程(在 Windows 上的 x86 中使用)。
coro.begin 和 coro.free 内在函数忽略对齐方式。
进行必要的更改,以确保协程优化与 LTO 一起工作。
更多测试,更多测试,更多测试