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 函式,該函式會呼叫 協程內建函式 來定義協程的結構。然後,在最一般的情況下,協程函式會被協程降低過程重寫為「ramp 函式」,即協程的初始入口點,它會一直執行到第一個掛起點。原始協程函式的其餘部分會被拆分為若干個「恢復函式」。任何必須在掛起之間持續存在的狀態都會儲存在協程框架中。恢復函式必須能夠以某種方式處理「正常」恢復(繼續協程的正常執行)或「異常」恢復(必須在不嘗試掛起協程的情況下解除協程)。

切換恢復降低

在 LLVM 的標準切換恢復降低中,透過使用 llvm.coro.id 來指示,協程框架會作為「協程物件」的一部分儲存,該物件表示協程特定調用的控制代碼。所有協程物件都支援通用的 ABI,允許在不了解協程實現的情況下使用某些功能。

  • 可以使用 llvm.coro.done 查詢協程物件是否已完成。

  • 如果協程物件尚未完成,則可以使用 llvm.coro.resume 正常恢復它。

  • 可以使用 llvm.coro.destroy 銷毀協程物件,使協程物件失效。即使協程已正常完成,也必須單獨執行此操作。

  • 可以使用 llvm.coro.promise 從協程物件中投影出已知大小和對齊方式的「Promise」儲存空間。必須編譯協程實現以定義大小和對齊方式相同的 Promise。

一般來說,在協程物件執行時以任何這些方式與其互動都會導致未定義的行為。

協程函式會被拆分為三個函式,表示控制可以進入協程的三種不同方式:

  1. 最初調用的 ramp 函式,它接受任意參數並返回指向協程物件的指標;

  2. 協程恢復函式,在恢復協程時調用,它接受指向協程物件的指標並返回 void

  3. 協程銷毀函式,在銷毀協程時調用,它接受指向協程物件的指標並返回 void

由於恢復和銷毀函式在所有掛起點之間共享,因此掛起點必須將活動掛起的索引儲存在協程物件中,並且恢復/銷毀函式必須切換到該索引才能返回到正確的點。因此,這種降低方式的名稱由此而來。

指向恢復和銷毀函式的指標儲存在協程物件中已知的偏移量處,這些偏移量對於所有協程都是固定的。已完成的協程使用空恢復函式表示。

用於分配和釋放協程物件的內建函式有一個稍微複雜的協定。它很複雜,以便允許由於內聯而省略分配。下面將更詳細地討論此協定。

前端可以生成代碼以直接調用協程函式;這將成為對 ramp 函式的調用,並將返回指向協程物件的指標。前端應始終使用相應的內建函式來恢復或銷毀協程。

返回延續降低

在返回延續降低中,透過使用 llvm.coro.id.retconllvm.coro.id.retcon.once 來指示,ABI 的某些方面必須由前端更明確地處理。

在這個降級過程中,每個暫停點都接受一個「產生的值」列表,這些值將與一個函式指標(稱為延續函式)一起返回給呼叫者。 協程通過簡單地呼叫此延續函式指標來恢復。 原始協程被分為 ramp 函式,然後是任意數量的這些延續函式,每個暫停點一個。

LLVM 實際上支援兩種密切相關的返回延續降級

  • 在正常的返回延續降級中,協程可能會暫停自身多次。 這意味著延續函式本身會返回另一個延續指標,以及一個產生的值列表。

    協程通過返回空延續指標來指示它已運行完成。 任何產生的值都將是 未定義的,應該被忽略。

  • 在產生一次返回延續降級中,協程必須準確地暫停自身一次(或拋出異常)。 ramp 函式返回一個延續函式指標和產生的值,當協程運行完成時,延續函式可以選擇返回普通結果。

協程框架維護在一個固定大小的緩衝區中,該緩衝區被傳遞給 coro.id 內建函式,該內建函式靜態地保證了一定的尺寸和對齊方式。 必須將相同的緩衝區傳遞給延續函式。 如果緩衝區不足,協程將分配內存,在這種情況下,它需要至少將該指標存儲在緩衝區中; 因此緩衝區必須始終至少是指標大小。 協程如何使用緩衝區可能會因暫停點而異。

除了緩衝區指標外,延續函式還接受一個參數,指示協程是正常恢復(零)還是異常恢復(非零)。

在將返回延續協程完全內嵌到呼叫者之後,LLVM 目前無法有效地靜態消除分配。 如果 LLVM 的協程支援主要用於低級降級並且預計在 pipeline 的早期應用內嵌,則這可能是可以接受的。

異步降級

在異步延續降級中,通過使用 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;
}

降級會將異步協程拆分為一個 ramp 函式和每個暫停點一個恢復函式。

呼叫端、暫停點和回到恢復函式的控制流程傳遞方式,由前端決定。

暫停點會接收一個函式及其引數。該函式旨在模擬轉移至被呼叫函式的過程。它會透過降低層級進行尾端呼叫,因此必須與非同步協程具有相同的簽章和呼叫慣例。

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
   }
}

這個協程會呼叫某個函式 print,並將值 n 作為引數,然後暫停執行。每次這個協程恢復時,它都會再次呼叫 print,並將一個比上次大的引數傳入。這個協程永遠不會自行完成,必須明確地銷毀。如果我們將這個協程與上一節中顯示的 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)時,控制流程會發生什麼變化。

協程轉換

協程降低層級的步驟之一是構建協程框架。會分析定義-使用鏈,以確定哪些物件需要在暫停點之間保持活動狀態。在上一節中顯示的協程中,虛擬暫存器 %inc 的使用與定義被一個暫停點隔開,因此,它不能駐留在堆疊框架上,因為一旦協程被暫停並且控制權返回給呼叫端,堆疊框架就會消失。會在協程框架中配置一個 i32 位置,並根據需要將 %inc 溢出並從該位置重新載入。

我們還會儲存恢復和銷毀函式的位址,以便在編譯時無法靜態確定協程的身分時,coro.resumecoro.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.resumef.destroy 中使用 switch 語句是可能的實現策略之一。我們還探討了另一種選擇,即為每個暫停點創建不同的 f.resume1f.resume2 等,並且不存儲索引,而是在每次暫停時更新恢復和銷毀函式指針。早期測試表明,目前的方法比後一種方法更容易被優化器處理,因此它是目前實現的降低策略。

分離儲存和暫停

在上一個示例中,設置恢復索引(或需要執行的其他狀態更改以準備恢復協程)與暫停協程同時發生。但是,在某些情況下,需要控制何時準備好恢復協程以及何時暫停協程。

在以下示例中,協程表示由異步操作 async_op1async_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_op1async_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]

協程 Promise

協程作者或前端可以指定一個區別的 alloca 來與協程進行通訊。這個區別的 alloca 被稱為 **協程 promise**,並作為第二個參數提供給 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 內建函數來訪問協程 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 語法中的 case)數量的最佳化機會。

以下是一個函數的範例,該函數會持續恢復協程,直到到達最終暫停點,然後銷毀協程

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);
}

內建函數

協程操作內建函數

本節中描述的內建函數用於操作現有的協程。它們可以用於任何恰好具有指向 協程框架 或指向 協程 promise 的指標的函數中。

‘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 為 false,則第一個參數是指向協程的控制代碼。否則,它是指向協程承諾的指標。

第二個參數是承諾的對齊需求。如果前端將 %promise = alloca i32 指定為承諾,則 coro.promise 的對齊參數應為目標平台上 i32 的對齊。如果前端將 %promise = alloca i32, align 16 指定為承諾,則對齊參數應為 16。此參數只接受常數。

第三個參數是一個布林值,表示轉換的方向。如果 from 為 true,則內建函式會傳回給定指向承諾之指標的協程控制代碼。如果 from 為 false,則內建函式會從協程控制代碼傳回指向承諾的指標。此參數只接受常數。

語意:

在沒有協程承諾的協程上使用此內建函式會導致未定義的行為。可以讀取和修改目前正在執行的協程的協程承諾。協程作者和協程使用者有責任確保沒有資料競爭。

範例:
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.idcoro.alloccoro.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 context 參數。

第四個參數是 async function pointer 結構的地址。Lowering 將通過將協程框架大小需求添加到此內建函數第一個參數指定的初始大小需求來更新此結構中的上下文大小需求。

語義:

前端應為每個協程發出恰好一個 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 的元素類型,按順序排列;

  • 否則,它就是協程函數的返回類型。

結果類型序列的第一個元素必須是指針類型;延續函數將被強制轉換為此類型。序列的其餘部分是「產生的類型」,並且協程中的任何暫停都必須採用這些類型的參數。

參數:

第一個和第二個參數是作為第三個參數提供的緩衝區的預期大小和對齊方式。它們必須是常數。

第四個參數必須是對全局函數的引用,稱為「延續原型函數」。任何延續函數的類型、調用約定和屬性都將取自此聲明。原型函數的返回類型必須與當前函數的返回類型匹配。第一個參數類型必須是指針類型。第二個參數類型必須是整數類型;它只會被用作布林標誌。

第五個參數必須是對將用於分配內存的全局函數的引用。它不能失敗,也不能通過返回 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 替換為適當的協程控點值。

如果此 coro.end 位於由於例外狀況而離開協程主體的展開序列的一部分區塊中,則第二個參數應為 true,否則為 false

非簡單(非 null)權杖參數只能針對唯一暫停返回延續協程指定,其中它必須是由「llvm.coro.end.results」內建函式產生的權杖值。

展開區段中的 coro.end 呼叫只允許 null 權杖。

語義:

此內建函式的目的是允許前端標記僅在協程的初始呼叫期間相關的清除和其他程式碼,並且不應出現在恢復和銷毀部分中。

在返回延續降低中,llvm.coro.end 會完全銷毀協程框架。如果第二個參數為 false,它也會以 null 延續指標從協程返回,並且下一條指令將無法到達。如果第二個參數為 true,它會穿透,以便後續邏輯可以恢復展開。在一次性協程中,在沒有先到達 llvm.coro.suspend.retcon 的情況下到達非展開 llvm.coro.end 的行為未定義。

本節的其餘部分描述了切換恢復降低下的行為。

當協程被分成開始、恢復和銷毀部分時,會降低此內建函式。在開始部分,它是不進行任何操作,在恢復和銷毀部分,它被替換為 ret void 指令,並且包含 coro.end 指令的區塊的其餘部分被丟棄。在登陸區中,它被替換為適當的指令以展開至呼叫者。coro.end 的處理方式因目標是使用登陸區還是 WinEH 例外模型而異。

對於基於登陸區的例外模型,預計前端使用 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

ret void

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 替換為適當的協程控點值。

如果此 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-case 的基本區塊。如果通過 coro.destroy 恢復,它將繼續執行 1-case 指示的基本區塊。要暫停,協程將繼續執行預設標籤。

如果 suspend intrinsic 被標記為 final,它可以將 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 降低來說將是一個更合適的功能。

引數:

引數的類型必須與協程的產生的類型序列完全匹配。它們將與下一個延續函式一起,從 ramp 和延續函式變成返回值。

語義:

內建函式的結果表示協程是否應異常恢復(非零)。

在正常的協程中,如果協程在異常恢復後執行對 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_awaitawait_suspend 區塊本質上與協程的執行非同步。將其正常地內嵌到未分割的協程中可能會導致編譯錯誤,因為協程 CFG 錯誤地表示了程式的真實控制流程:在 await_suspend 中發生的事件不能保證在協程恢復之前發生,並且在協程恢復後發生的事件(包括其退出和協程框架的潛在解除分配)不能保證僅在 await_suspend 結束後發生。

這個版本的內建函式對應於 ‘void awaiter.await_suspend(...)’ 變體。

引數:

第一個引數是指向 awaiter 物件的指標。

第二個引數是指向當前協程框架的指標。

第三個引數是指向封裝 await-suspend 邏輯的包裝函式的指標。其簽章必須為

declare void @await_suspend_function(ptr %awaiter, ptr %hdl)
語義:

內建函式必須在相應的 coro.savecoro.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_awaitawait_suspend 區塊本質上與協程的執行非同步。將其正常地內嵌到未分割的協程中可能會導致編譯錯誤,因為協程 CFG 錯誤地表示了程式的真實控制流程:在 await_suspend 中發生的事件不能保證在協程恢復之前發生,並且在協程恢復後發生的事件(包括其退出和協程框架的潛在解除分配)不能保證僅在 await_suspend 結束後發生。

這個版本的內建函式對應於 ‘bool awaiter.await_suspend(...)’ 變體。

引數:

第一個引數是指向 awaiter 物件的指標。

第二個引數是指向當前協程框架的指標。

第三個引數是指向封裝 await-suspend 邏輯的包裝函式的指標。其簽章必須為

declare i1 @await_suspend_function(ptr %awaiter, ptr %hdl)
語義:

內建函式必須在相應的 coro.savecoro.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_awaitawait_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.savecoro.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.framecoro.donecoro.promise 內建函式。

CoroSplit

CoroSplit 過程會建置協程框架,並將恢復和銷毀部分分離成獨立的函式。此過程也會降低 coro.await.suspend.voidcoro.await.suspend.boolcoro.await.suspend.handle 內建函式。

CoroElide

CoroElide 過程會檢查內聯協程是否符合堆積配置省略最佳化的條件。如果是,它會將 coro.begin 內建函式替換為放置在其呼叫端上的協程框架位址,並將 coro.alloccoro.free 內建函式分別替換為 falsenull,以移除釋放程式碼。此過程也會盡可能將 coro.resumecoro.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 = !{}

需要注意的事項

  1. 當 coro.suspend 返回 -1 時,協程會被暫停,並且該協程可能已被銷毀(因此框架已被釋放)。我們無法在暫停路徑上存取框架上的任何內容。然而,沒有任何東西可以阻止編譯器沿著該路徑移動指令(例如 LICM),這可能會導致釋放後使用。目前,我們針對具有 coro.suspend 的迴圈停用了 LICM,但一般問題仍然存在,需要一個通用的解決方案。

  2. 利用生命週期內建函數來處理進入協程框架的資料。對於保留在配置中的資料,請保留生命週期內建函數。

  3. CoroElide 優化過程依賴協程 ramp 函數被內聯。將 ramp 函數進一步拆分將是有益的,以增加其被內聯到其呼叫端的機會。

  4. 設計一種約定,使其能夠跨 ABI 邊界應用協程堆省略優化。

  5. 無法處理具有 inalloca 參數的協程(在 Windows 上的 x86 中使用)。

  6. coro.begin 和 coro.free 內建函數會忽略對齊。

  7. 進行必要的更改,以確保協程優化與 LTO 配合使用。

  8. 更多測試、更多測試、更多測試