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 函數,該函數呼叫了協程內建函數,這些內建函數定義了協程的結構。然後,在最一般的情況下,協程函數會被協程降低 Pass 重寫,成為 “ramp function”,即協程的初始進入點,它會執行直到首次到達暫停點。原始協程函數的其餘部分會被拆分到一些 “恢復函數” 中。任何必須在暫停期間持續存在的狀態都會儲存在協程框架中。恢復函數必須能夠以某種方式處理 “正常” 恢復 (它會繼續協程的正常執行) 或 “異常” 恢復 (它必須在不嘗試暫停協程的情況下解除協程)。

切換恢復降低

在 LLVM 的標準切換恢復降低中,由 llvm.coro.id 的使用表示,協程框架作為 “協程物件” 的一部分儲存,該物件表示特定協程調用的句柄。所有協程物件都支援通用的 ABI,允許在不了解協程實作的任何情況下使用某些功能

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

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

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

  • 已知具有特定大小和對齊方式的 “Promise” 儲存,可以使用 llvm.coro.promise 從協程物件中投影出來。協程實作必須已編譯為定義相同大小和對齊方式的 Promise。

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

協程函數被拆分為三個函數,代表控制權可以進入協程的三種不同方式

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

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

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

由於恢復和銷毀函數在所有暫停點之間共享,因此暫停點必須在協程物件中儲存活動暫停的索引,並且恢復/銷毀函數必須根據該索引進行切換,以返回到正確的點。因此,得名這種降低方式。

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

存在一個有點複雜的內建函數協定,用於分配和釋放協程物件。它的複雜性是為了允許由於內聯而省略分配。此協定將在下面進一步詳細討論。

前端可能會產生程式碼來直接呼叫協程函數;這將變成對 ramp function 的呼叫,並將返回指向協程物件的指標。前端應始終使用相應的內建函數來恢復或銷毀協程。

返回接續降低

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

在這種降低方式中,每個暫停點都採用一個 “yielded values” 列表,這些值與函數指標 (稱為 continuation function) 一起返回給呼叫者。協程通過簡單地呼叫此 continuation function 指標來恢復。原始協程被分為 ramp function,然後是任意數量的這些 continuation function,每個暫停點一個。

LLVM 實際上支援兩種密切相關的返回接續降低方式

  • 在正常的返回接續降低中,協程可能會多次暫停自身。這意味著 continuation function 本身會返回另一個 continuation 指標,以及一個 yielded values 列表。

    協程通過返回空值 continuation 指標來指示它已執行完成。任何 yielded values 都將是 undef,應被忽略。

  • 在 yield-once 返回接續降低中,協程必須恰好暫停自身一次 (或拋出異常)。ramp function 返回 continuation function 指標和 yielded values,當協程執行完成時,continuation function 可以選擇性地返回普通結果。

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

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

在將返回接續協程完全內聯到呼叫者之後,LLVM 目前在靜態消除分配方面效率不高。如果 LLVM 的協程支援主要用於低階降低,並且預期內聯在管道中較早應用,則這可能是可以接受的。

非同步降低

在非同步接續降低中,由 llvm.coro.id.async 的使用表示,控制流的處理必須由前端明確處理。

在這種降低方式中,假設協程將當前的 async context 作為其參數之一 (參數位置由 llvm.coro.id.async 確定)。它用於整理協程的參數和返回值。因此,非同步協程返回 void

define swiftcc void @async_coroutine(ptr %async.ctxt, ptr, ptr) {
}

跨暫停點存活的值需要儲存在協程框架中,以便在 continuation function 中可用。此框架作為 async context 的尾部儲存。

每個暫停點都採用一個 context projection function 參數,該參數描述如何取得 continuation 的 async context,並且每個暫停點都有一個相關聯的 resume function,由 llvm.coro.async.resume 內建函數表示。協程通過呼叫此 resume function 並將 async context 作為其參數之一來恢復。resume function 可以通過應用 context projection function 來恢復其 (呼叫者的) async context,該函數由前端作為參數提供給 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

前端應提供一個 async function pointer 結構,通過 llvm.coro.id.async 的參數與每個非同步協程相關聯。async context 的初始大小和對齊方式必須作為參數提供給 llvm.coro.id.async 內建函數。降低將使用協程框架需求更新大小條目。前端負責為 async context 分配記憶體,但可以使用 async function pointer 結構來取得所需的大小。

struct async_function_pointer {
  uint32_t relative_function_pointer_to_async_impl;
  uint32_t context_size;
}

降低會將非同步協程拆分為 ramp function 和每個暫停點一個恢復函數。

控制流如何在呼叫者、暫停點和返回恢復函數之間傳遞,由前端決定。

暫停點採用一個函數及其參數。該函數旨在模擬傳輸到被呼叫函數。它將被降低尾部呼叫,因此必須具有與非同步協程相同的簽名和呼叫約定。

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,參數比上次大一。此協程永遠不會自行完成,必須明確銷毀。如果我們將此協程與上一節中顯示的 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 內建函數被最佳化 Pass (例如 jump-threading) 複製時很有用。

cleanup 區塊銷毀協程框架。coro.free 內建函數,在給定協程句柄的情況下,返回要釋放的記憶體區塊的指標,如果協程框架不是動態分配的,則返回 nullcleanup 區塊在協程自行執行完成或通過呼叫 coro.destroy 內建函數銷毀時進入。

suspend 區塊包含在協程執行完成或暫停時要執行的程式碼。coro.end 內建函數標記協程需要將控制權返回給呼叫者的點 (如果它不是協程的初始調用)。

loop 區塊表示協程的主體。coro.suspend 內建函數與以下 switch 結合使用,指示當協程暫停 (預設情況)、恢復 (case 0) 或銷毀 (case 1) 時控制流會發生什麼情況。

協程轉換

協程降低的步驟之一是建構協程框架。分析 def-use 鏈以確定哪些物件需要在跨暫停點保持存活。在上一節中顯示的協程中,虛擬暫存器 %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 來避免動態分配。

在 entry 區塊中,我們將呼叫 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)

在 cleanup 區塊中,我們將使釋放協程框架取決於 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);
}

自訂 ABI 和外掛程式庫

外掛程式庫可以擴展協程降低,使各種使用者能夠利用協程轉換 Pass。現有的協程降低通過以下方式擴展

  1. 定義繼承自現有 ABI 的自訂 ABI,

  2. 在建構 CoroSplit Pass 時,為自訂 ABI 提供一個產生器列表,以及

  3. 使用 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 的預設處理方式違反了這些規則 — 拆分將嘗試用 coro 框架中的條目替換 alloca,這可能會導致嘗試將其作為 swifterror 參數傳遞。要在拆分函數中傳遞 swifterror 參數,我們仍然需要保留 alloca;但我們也可能需要 coro 框架插槽,因為有用的資料 (理論上) 可以儲存在預拆分協程中跨暫停的 swifterror alloca 插槽中。當拆分協程時,因此有必要同時保留框架插槽和 alloca 本身,然後使它們保持同步。

內建函數

協程操作內建函數

本節中描述的內建函數用於操作現有的協程。它們可以用於任何碰巧具有指向協程框架或指向協程 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’ 內建函數取得指向協程 promise的指標 (給定切換恢復協程句柄) 以及反之亦然。

參數:

如果 from 為 false,則第一個參數是指向協程的句柄。否則,它是指向協程 promise 的指標。

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

第三個參數是一個布林值,指示轉換的方向。如果 from 為 true,則內建函數返回一個協程句柄 (給定指向 promise 的指標)。如果 from 為 false,則內建函數從協程句柄返回指向 promise 的指標。此參數僅接受常數。

語意:

在沒有協程 promise 的協程上使用此內建函數會導致未定義的行為。可以讀取和修改目前正在執行的協程的協程 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
}

協程結構內建函數

本節中描述的內建函數在協程內使用,以描述協程結構。它們不應在協程外部使用。

‘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’ 返回的 token,用於識別協程。

第二個參數是指向記憶體區塊的指標,如果協程框架是動態分配的,則協程框架將儲存在該記憶體區塊中。對於返回接續協程,此指標將被忽略。

語意:

根據協程框架中物件的對齊要求和/或程式碼產生緊湊性的原因,從 coro.begin 返回的指標可能與 %mem 參數存在偏移量。(如果表達相對資料存取的指令可以用小的正負偏移量更緊湊地編碼,則這可能是有益的)。

每個協程,前端應發出正好一個 coro.begin 內建函數。

‘llvm.coro.begin.custom.abi’ 內建函數

declare ptr @llvm.coro.begin.custom.abi(token <id>, ptr <mem>, i32)
概述:

llvm.coro.begin.custom.abi’ 內建函數用於代替 coro.begin 內建函數,後者有一個額外的參數來指定協程的自訂 ABI。返回值與 coro.begin 內建函數的返回值相同。

參數:

第一個和第二個參數與 coro.begin 內建函數的參數相同。

第三個參數是傳遞給 CoroSplit 階段的產生器列表的 i32 索引,用於指定此協程的自訂 ABI 產生器。

語意:

語意與 coro.begin 內建指令的語意相同。

‘llvm.coro.free’ 內建指令

declare ptr @llvm.coro.free(token %id, ptr <frame>)
概述:

llvm.coro.free’ 內建指令會傳回指向記憶體區塊的指標,該記憶體區塊儲存協程框架;如果此協程實例未使用動態分配的記憶體來儲存其協程框架,則傳回 null。返回延續協程不支援此內建指令。

參數:

第一個參數是呼叫 ‘llvm.coro.id’ 返回的 token,用於識別協程。

第二個參數是指向協程框架的指標。這應該是先前 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’ 返回的 token,用於識別協程。

語意:

前端應針對每個協程最多發出一個 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’ 內建指令會傳回識別非同步協程的符記。

參數:

第一個參數提供前端要求的 非同步內容 的初始大小。降低階段會將框架儲存所需的大小加入此大小,並將該值儲存到 非同步函數指標

第二個參數是 非同步內容 記憶體的對齊保證。前端保證記憶體將以此值對齊。

第三個參數是目前協程中的 非同步內容 參數。

第四個參數是 非同步函數指標 結構的位址。降低階段會透過將協程框架大小需求加入此內建指令的第一個參數指定的初始大小需求,來更新此結構中的內容大小需求。

語意:

前端應針對每個協程恰好發出一個 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

非平凡(非 none)符記參數只能為唯一暫停返回延續協程指定,其中它必須是由 ‘llvm.coro.end.results’ 內建指令產生的符記值。

在解除堆疊區段中,僅允許 none 符記用於 coro.end 呼叫

語意:

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

在返回延續降低中,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 例外處理模型,前端應附加參考封閉 cleanuppad 的 funclet 捆綁包,如下所示

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

如果存在 funclet 捆綁包,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
將協程標記為已完成

落地墊

將協程標記為已完成

將協程標記為已完成

‘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 指示的基本區塊。若要暫停,協程會繼續進行到預設標籤。

如果暫停內建指令被標記為最終,它可以將 true 分支視為無法到達,並執行可以利用該事實的最佳化。

‘llvm.coro.save’ 內建指令

declare token @llvm.coro.save(ptr <handle>)
概述:

llvm.coro.save’ 標記協程需要更新其狀態以準備恢復以被視為暫停(因此符合恢復條件)的點。合併兩個 ‘llvm.coro.save’ 呼叫是非法的,除非它們的 ‘llvm.coro.suspend’ 使用者也被合併。因此,‘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 內建指令。

CoroAnnotationElide

此階段會尋找所有「必須省略」的協程用法,並將 coro.begin 內建指令替換為放置在其呼叫者上的協程框架位址,並分別將 coro.alloccoro.free 內建指令替換為 falsenull,以移除釋放程式碼。

CoroElide

CoroElide 階段會檢查內聯協程是否符合堆積分配省略最佳化條件。如果是,則它會將 coro.begin 內建指令替換為放置在其呼叫者上的協程框架位址,並分別將 coro.alloccoro.free 內建指令替換為 falsenull,以移除釋放程式碼。此階段也會在可能的情況下,將 coro.resumecoro.destroy 內建指令替換為對特定協程的恢復和銷毀函數的直接呼叫。

CoroCleanup

此階段稍後運行,以降低所有未被早期階段替換的協程相關內建指令。

屬性

coro_only_destroy_when_complete

當協程標記為 coro_only_destroy_when_complete 時,表示協程在銷毀時必須到達最終暫停點。

此屬性目前僅適用於切換恢復協程。

coro_elide_safe

當標記為 coro_elide_safe 的切換 ABI 協程 f 的 Call 或 Invoke 指令時,CoroSplitPass 會產生 f.noalloc ramp 函數。f.noalloc 比其原始 ramp 函數 f 多一個參數,這是指向已分配框架的指標。f.noalloc 也會抑制可能受 @llvm.coro.alloc@llvm.coro.free 保護的任何分配或釋放。

CoroAnnotationElidePass 會在可能的情況下執行堆積省略。請注意,對於遞迴或相互遞迴函數,這種省略通常是不可能的。

Metadata

coro.outside.frame’ Metadata

coro.outside.frame metadata 可以附加到 alloca 指令,以表示不應將其提升到協程框架,這在發出內部控制機制時,對於前端過濾掉 allocas 非常有用。此外,此 metadata 僅用作標誌,因此關聯的節點必須為空。

%__coro_gro = alloca %struct.GroType, align 1, !coro.outside.frame !0

...
!0 = !{}

需注意之處

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

  2. 利用生命週期內建指令來處理進入協程框架的資料。對於保留在 allocas 中的資料,保持生命週期內建指令不變。

  3. CoroElide 最佳化階段依賴協程 ramp 函數進行內聯。進一步分割 ramp 函數將是有益的,以增加其內聯到其呼叫者的機會。

  4. 設計一種慣例,使其有可能跨 ABI 邊界應用協程堆積省略最佳化。

  5. 無法處理帶有 inalloca 參數的協程(用於 Windows 上的 x86)。

  6. 對齊方式被 coro.begin 和 coro.free 內建指令忽略。

  7. 進行必要的變更,以確保協程最佳化與 LTO 一起運作。

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