LLVM 的垃圾回收安全點¶
狀態¶
本文件描述了一組 LLVM 的擴展,用於支援垃圾回收。到目前為止,這些機制已經在商業 Java 實現中得到了充分的驗證,並已發布了使用完全重定位收集器的版本。還有一些地方可能仍然存在錯誤;這些將在下面說明。
它們仍然被列為「實驗性」,表示在不同版本之間不提供向前或向後相容性保證。如果您的用例需要某種形式的向前相容性保證,請在 llvm-dev 郵件列表中提出問題。
LLVM 仍然支援使用 gcroot
內建函式來支援保守的垃圾回收機制。gcroot
機制目前主要具有歷史意義,但有一個例外 - 它對陰影堆疊的實現已被許多語言前端成功使用,並且仍然受到支援。
概述與核心概念¶
為了收集無效物件,垃圾收集器必須能夠識別執行程式碼中包含的任何物件引用,並且根據收集器的不同,可能會更新它們。收集器不需要在程式碼的所有點都獲得這些資訊 - 這會使問題變得更加困難 - 而只需要在執行中稱為「安全點」的定義良好的點獲得這些資訊。對於大多數收集器來說,至少追蹤每個唯一指標值的一個副本就足夠了。但是,對於希望直接從執行程式碼中重新定位物件的收集器,則需要更高的標準。
另一個挑戰是編譯器可能會計算指向分配外部甚至指向另一個分配中間的結果(「衍生指標」)。此中間值的最終使用必須產生分配範圍內的地址,但垃圾收集器可能會看到此類「外部衍生指標」。鑑於此,垃圾收集器不能安全地依賴地址的運行時值來指示与其關聯的物件。如果垃圾收集器希望移動任何物件,則編譯器必須為每個指標提供到其分配的指示的映射。
為了簡化收集器與編譯後代碼之間的互動,大多數垃圾收集器都圍繞著三個抽象概念來組織:加載屏障、存儲屏障和安全點。
加載屏障是在機器加載指令之後、使用加載值之前的代碼片段。根據收集器的不同,可能需要為所有加載、僅針對特定類型(在原始源語言中)的加載或完全不需要加載屏障。
類似地,存儲屏障是在機器存儲指令之前、計算存儲值之後運行的代碼片段。存儲屏障最常見的用途是在分代垃圾收集器中更新“卡片表”。
安全點是允許編譯後代碼可見的指針(即當前在寄存器或堆棧中)發生變化的位置。在安全點完成後,實際的指針值可能會有所不同,但指向的“對象”(從源語言的角度來看)不會改變。
請注意,“安全點”一詞的含義有些過於寬泛。它既指機器狀態可解析的位置,也指將應用程序線程帶到收集器可以安全地使用該信息的點的協調協議。本文檔中使用的術語“狀態點”僅指前者。
本文檔重點介紹最後一項 - 編譯器對生成代碼中安全點的支持。我們假設外部機制已決定在何處放置安全點。從我們的角度來看,所有安全點都將是函數調用。為了支持直接從編譯代碼中的值到達的對象的重定位,收集器必須能夠
識別安全點處的每個指針副本(包括編譯器本身引入的副本),
識別每個指針與哪個對象相關,以及
潛在地更新每個副本。
本文檔描述了基於 LLVM 的編譯器如何向語言運行時/收集器提供此信息,並確保可以根據需要讀取和更新所有指針。
抽象機器模型¶
在較高的層面上,LLVM 已擴展為支持編譯到抽象機器,該機器使用適用於表示對對象的垃圾回收引用的非整數指針類型擴展了實際目標。特別是,此類非整數指針類型沒有定義到整數表示的映射。這種語義上的怪癖允許運行時為程序中的每個點選擇一個整數映射,從而允許在不產生可見效果的情況下對對象進行重定位。
此高層抽象機器模型用於大多數優化器。因此,轉換過程不需要擴展以查看顯式重定位序列。在開始代碼生成之前,我們將表示切換為顯式形式。選擇用於降低的精確位置是一個實現細節。
請注意,抽象機器模型的大多數價值都來自於需要對潛在可重定位對象進行建模的收集器。對於僅支持非重定位收集器的編譯器,您可能希望考慮從完全顯式形式開始。
警告:非整數指針的定義中目前存在一個已知的語義漏洞,該漏洞尚未在上游解決。要解決此問題,您需要禁用加載的推測,除非已知內存類型(非整數指針與任何其他類型)沒有更改。也就是說,如果導致將非整數指針值加載為任何其他類型或反之亦然,則推測加載是不安全的。在實踐中,此限制很好地隔離在 ValueTracking.cpp 中的 isSafeToSpeculate。
顯式表示¶
前端可以直接生成這種低級別的顯式形式,但這樣做可能會抑制優化。相反,建議使用帶有重定位收集器的編譯器來針對剛才描述的抽象機器模型。
顯式方法的核心是以垃圾收集器可能執行的更新在 IR 中顯式可見的方式構造(或重寫)IR。這樣做需要我們
為每個可能被重定位的指針創建一個新的 SSA 值,並確保在安全點之後無法訪問原始(未重定位)值的使用,
以對編譯器不透明的方式指定重定位,以確保優化器無法在狀態點之後引入未重定位值的新使用。這可以防止優化器執行不正確的優化。
記錄每個狀態點的活動指針(以及它們關聯的分配)的映射。
在最抽象的層級上,插入安全點可以被認為是用對多返回值函數的調用替換調用指令,該函數既調用調用的原始目標,返回其結果,又返回任何指向垃圾收集對象的活動指針的更新值。
請注意,識別指向垃圾收集值的所有活動指針、轉換 IR 以公開提供每個此類活動指針的基對象的指針,以及正確插入所有內部函數的任務顯然超出了本文檔的範圍。推薦的方法是使用下面描述的實用程序遍歷。
這個抽象函數調用由一系列內部函數調用具體表示,這些調用統稱為“狀態點重定位序列”。
讓我們考慮 LLVM IR 中的一個簡單調用
declare void @foo()
define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
gc "statepoint-example" {
call void @foo()
ret ptr addrspace(1) %obj
}
根據我們的語言,我們可能需要在執行 foo
期間允許安全點。如果是這樣,我們需要讓收集器更新當前框架中的局部值。如果我們不這樣做,我們最終從調用返回時將訪問一個潛在的無效引用。
在本例中,我們需要重定位 SSA 值 %obj
。由於我們實際上無法更改 SSA 值 %obj
中的值,因此我們需要引入一個新的 SSA 值 %obj.relocated
,它表示安全點之後 %obj
的潛在更改值,並相應地更新任何後續使用。產生的重定位序列是
define ptr addrspace(1) @test(ptr addrspace(1) %obj)
gc "statepoint-example" {
%safepoint = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr addrspace(1) %obj)]
%obj.relocated = call ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %safepoint, i32 0, i32 0)
ret ptr addrspace(1) %obj.relocated
}
理想情況下,此序列將表示為一個 M 個參數、N 個返回值的函數(其中 M 是要重定位的值的數量 + 原始調用參數,N 是原始返回值 + 每個重定位的值),但 LLVM 不容易支持這種表示。
相反,狀態點內部函數標記安全點或狀態點的實際位置。狀態點返回一個標記值(僅在編譯時存在)。要獲得調用的原始返回值,我們使用 gc.result
內部函數。要依次獲取每個指針的重定位,我們使用帶有適當索引的 gc.relocate
內部函數。請注意,gc.relocate
和 gc.result
都與狀態點綁定。該組合形成一個“狀態點重定位序列”,並表示可解析調用或“狀態點”的完整性。
當降低時,這個例子將生成以下 x86 彙編代碼
.globl test1
.align 16, 0x90
pushq %rax
callq foo
.Ltmp1:
movq (%rsp), %rax # This load is redundant (oops!)
popq %rdx
retq
每個可能被重定位的值都已溢出到堆棧,並且該位置的記錄已記錄到堆棧映射部分。如果垃圾收集器需要在調用期間更新這些指針中的任何一個,它確切地知道要更改什麼。
我們示例中堆棧映射部分的相關部分是
# This describes the call site
# Stack Maps: callsite 2882400000
.quad 2882400000
.long .Ltmp1-test1
.short 0
# .. 8 entries skipped ..
# This entry describes the spill slot which is directly addressable
# off RSP with offset 0. Given the value was spilled with a pushq,
# that makes sense.
# Stack Maps: Loc 8: Direct RSP [encoding: .byte 2, .byte 8, .short 7, .int 0]
.byte 2
.byte 8
.short 7
.long 0
此範例取自 RewriteStatepointsForGC 工具程式的測試。 因此,可以使用以下命令輕鬆檢查其完整的 StackMap。
opt -rewrite-statepoints-for-gc test/Transforms/RewriteStatepointsForGC/basics.ll -S | llc -debug-only=stackmaps
非轉移式 GC 的簡化¶
對於非轉移式收集器來說,上一個範例中的一些複雜性是不必要的。 雖然非轉移式收集器仍然需要有關哪些位置包含活動參考的資訊,但它不需要表示明確的轉移。 因此,前面描述的顯式降低可以簡化為移除所有 gc.relocate
內建函式呼叫,並保留原始參考值的用法。
以下是上一個範例針對非轉移式收集器的顯式降低
define void @manual_frame(ptr %a, ptr %b) gc "statepoint-example" {
%alloca = alloca ptr
%allocb = alloca ptr
store ptr %a, ptr %alloca
store ptr %b, ptr %allocb
call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 0, i32 0, ptr elementtype(void ()) @func, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr %alloca, ptr %allocb)]
ret void
}
記錄堆疊區域¶
除了先前描述的顯式轉移形式之外,狀態點基礎架構還允許在 gc 指標清單中列出 alloca。 可以在有或沒有額外的顯式 gc 指標值和轉移的情況下列出 Alloca。
狀態點運算元清單的 gc 區域中的 alloca 將導致堆疊區域的地址列在狀態點的堆疊映射中。
如果需要,此機制可用於描述顯式溢出槽。 然後,生成器有責任確保在安全點的任一側根據需要將值溢出/填充到 alloca。 請注意,無法為此類顯式指定的溢出槽指示相應的基指標,因此使用僅限於關聯的收集器可以從指標本身推導出對象基的值。
此機制可用於描述堆疊上包含參考的對象,前提是收集器可以從堆疊上的位置映射到描述收集器需要處理的參考的內部佈局的堆映射。
警告:目前,這種替代形式還沒有得到很好的運用。 建議謹慎使用此選項,並預計必須修復一些錯誤。 特別是,RewriteStatepointsForGC 工具程式目前沒有對 alloca 做任何處理。
基指標和衍生指標¶
「基指標」是指向配置(對象)起始地址的指標。 「衍生指標」是指從基指標偏移一定量的指標。 在轉移對象時,垃圾收集器需要能夠將與配置關聯的每個衍生指標轉移到與新地址相同的偏移量。
「內部衍生指標」保留在其關聯的配置範圍內。 因此,如果運行時系統知道配置的邊界,則可以在運行時找到基對象。
「外部衍生指標」位於關聯對象的範圍之外; 它們甚至可能落在*另一個*配置地址範圍內。 因此,垃圾收集器無法在運行時確定它們與哪個配置相關聯,因此需要編譯器支援。
內建函式 gc.relocate
支援一個顯式的運算元,用於描述與衍生指標相關聯的配置。這個運算元通常被稱為基底運算元,但嚴格來說不一定是基底指標,但它確實需要位於相關聯配置的邊界內。某些收集器可能要求運算元是實際的基底指標,而不仅仅是內部衍生指標。請注意,在降低過程中,即使基底在之後不再使用,基底和衍生指標運算元都需要在相關聯的呼叫安全點上保持活躍。
GC 轉換¶
作為一個實際的考量,許多垃圾回收系統允許感知收集器的程式碼(「受管程式碼」)呼叫不感知收集器的程式碼(「非受管程式碼」)。通常,此類呼叫也必須是安全點,因為希望允許收集器在執行非受管程式碼期間執行。此外,協調從受管程式碼到非受管程式碼的轉換通常需要在呼叫站點產生額外的程式碼,以通知收集器轉換。為了支援這些需求,可以將狀態點標記為 GC 轉換,並且可以將執行轉換所需的資料(如果有)作為附加參數提供給狀態點。
請注意,儘管在許多情況下,可以根據涉及的函數符號推斷狀態點是 GC 轉換(例如,從具有 GC 策略「foo」的函數到具有 GC 策略「bar」的函數的呼叫),但也必須支援作為 GC 轉換的間接呼叫。這一要求是決定要求明確標記 GC 轉換的驅動力。
讓我們重新審視上面給出的範例,這次將對 @foo
的呼叫視為 GC 轉換。根據我們的目標,轉換程式碼可能需要存取一些額外的狀態,以便通知收集器轉換。讓我們假設一個假設的 GC(有點缺乏想像力地命名為「hypothetical-gc」),它要求在呼叫非受管程式碼之前和之後必須寫入一個 TLS 變數。產生的重定位序列是
@flag = thread_local global i32 0, align 4
define i8 addrspace(1)* @test1(i8 addrspace(1) *%obj)
gc "hypothetical-gc" {
%0 = call token (i64, i32, void ()*, i32, i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, void ()* @foo, i32 0, i32 1, i32* @Flag, i32 0, i8 addrspace(1)* %obj)
%obj.relocated = call coldcc i8 addrspace(1)* @llvm.experimental.gc.relocate.p1i8(token %0, i32 7, i32 7)
ret i8 addrspace(1)* %obj.relocated
}
在降低過程中,這將導致指令選擇 DAG 看起來像這樣
CALLSEQ_START
...
GC_TRANSITION_START (lowered i32 *@Flag), SRCVALUE i32* Flag
STATEPOINT
GC_TRANSITION_END (lowered i32 *@Flag), SRCVALUE i32 *Flag
...
CALLSEQ_END
為了產生必要的轉換程式碼,「hypothetical-gc」支援的每個目標的後端都必須修改,以便在特定函數使用「hypothetical-gc」策略時,適當地降低 GC_TRANSITION_START
和 GC_TRANSITION_END
節點。假設已經為 X86 添加了這樣的降低,則生成的組譯程式碼將是
.globl test1
.align 16, 0x90
pushq %rax
movl $1, %fs:Flag@TPOFF
callq foo
movl $0, %fs:Flag@TPOFF
.Ltmp1:
movq (%rsp), %rax # This load is redundant (oops!)
popq %rdx
retq
請注意,上述設計尚未完全實現:特別是,沒有策略特定的降低,並且所有 GC 轉換都在呼叫指令之前和之後發出為單個無操作。這些無操作通常在無用機器指令消除期間由後端移除。
在抽象機器模型被 RewriteStatepointsForGC pass 降低到顯式狀態點重定位模型之前,任何衍生指標都可以使用 gc.get.pointer.base
和 gc.get.pointer.offset
內建函式分別獲取其基底指標和從基底指標開始的偏移量。這些內建函式由 RewriteStatepointsForGC pass 內聯,並且在此 pass 之後不得使用。
堆疊映射格式¶
執行階段或收集器可能需要讀取和/或更新的每個指標值的位址,會在產生的物件檔案的獨立區段中提供,如 PatchPoint 文件中所述。此特殊區段根據 堆疊映射格式 編碼。
一般預期是 JIT 編譯器會解析並捨棄這種格式;它在記憶體使用效率方面並不特別出色。如果您需要其他格式(例如,對於先行編譯器),請參閱下方 :ref: 未完成項目 <OpenWork> 底下的討論。
每個狀態點都會產生以下位置
描述呼叫目標呼叫慣例的常數。此常數是產生堆疊映射所使用的 LLVM 版本的有效 呼叫慣例識別碼。除了 LLVM 在其他地方針對這些識別碼提供的內容之外,沒有針對此常數做出額外的相容性保證。
描述傳遞給狀態點內建函式的旗標的常數
描述以下 deopt *位置* 數量的常數(非運算元)。如果沒有提供「deopt」組合,則為 0。
可變數量的 *位置*,「deopt」運算元組合中列出的每個 deopt 參數都有一個。目前,僅支援位元寬度為 64 位元或更少的 deopt 參數。如果 a) 呼叫站點的值為常數,且 b) 常數可以用少於 64 位元表示(假設零擴展到原始位元寬度),則只能指定和報告類型大於 64 位元的值。
可變數量的重定位記錄,每個記錄都由兩個 *位置* 組成。重定位記錄將在下方詳細說明。
每個重定位記錄都提供足夠的資訊,讓收集器可以重定位一個或多個衍生指標。每個記錄都由一對 *位置* 組成。記錄中的第二個元素表示需要更新的指標(或指標)。記錄中的第一個元素提供了一個指標,指向與正在重定位的指標相關聯的物件的基底。處理廣義衍生指標需要此資訊,因為指標可能超出原始配置的範圍,但仍然需要與配置一起重定位。此外
如果在狀態點之後使用基底指標,則保證基底指標也必須作為重定位對明確出現。
重定位記錄的數量可能少於 IR 狀態點中的 gc 參數。每個*唯一*對至少會出現一次;可能會有重複項。
每個記錄中的 *位置* 可能具有指標大小或指標大小的倍數。在後一種情況下,必須將記錄解釋為描述一系列指標及其對應的基底指標。如果 *位置* 的大小為 N x sizeof(pointer),則 *位置* 中將包含 N 個記錄,每個記錄一個指標。可以假設一對中的兩個 *位置* 的大小相同。
請注意,每個區段中使用的 *位置* 可能描述相同的實體位置。例如,堆疊位置可能顯示為 deopt 位置、gc 基底指標和 gc 衍生指標。
狀態點記錄的 StkMapRecord 的 LiveOut 區段將為空。
安全點語義和驗證¶
編譯程式碼在垃圾收集器方面的正確性的基本正確性屬性是動態的。必須確保沒有動態追蹤,使得涉及潛在重定位指標的操作在可以重定位它的安全點之後可觀察到。此處使用「可觀察到」表示外部觀察者可以觀察到此事件序列,從而排除在安全點之前執行操作的可能性。
要理解為什麼需要這種「可觀察後」屬性,請考慮對重新定位指標的原始副本執行的空值比較。 假設控制流遵循安全點,則無法從外部觀察空值比較是在安全點之前還是之後執行的。(請記住,原始值不會被安全點修改。) 編譯器可以自由地做出任何一種調度選擇。
實際實現的正確性屬性比這稍微強一些。 我們要求在可能重新定位的指標「可觀察後」可能已被重新定位的指標上沒有*靜態路徑*。 這比嚴格要求的要稍微強一些(因此可能會禁用一些其他有效的程式),但大大簡化了關於編譯程式碼正確性的推理。
根據構造,如果在原始 IR 中正確建立,則最佳化器將維持此屬性。 這是設計的關鍵不變量。
現有的 IR 驗證器傳遞已擴展為檢查大多數對其各自文件中提到的內建函式的局部限制。 LLVM 中的當前實現不檢查關鍵重新定位不變量,但這是正在開發此類驗證器的持續工作。 如果您有興趣試用當前版本,請在 llvm-dev 上提問。
用於插入安全點的實用程式傳遞¶
RewriteStatepointsForGC¶
RewriteStatepointsForGC 傳遞將函式的 IR 從上述抽象機器模型降低到重新定位的顯式安全點模型。 為此,它將使用 gc.statepoint
和關聯的完整重新定位序列(包括所有必需的 gc.relocates
)替換可能包含安全點輪詢的所有函式呼叫或調用。
此傳遞僅適用於設定了 UseRS4GC
標誌的 GCStrategy 實例。 設定了此標誌的兩個內建 GC 策略是“statepoint-example”和“coreclr”策略。
例如,給定此程式碼
define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
gc "statepoint-example" {
call void @foo()
ret ptr addrspace(1) %obj
}
該傳遞將產生此 IR
define ptr addrspace(1) @test_rs4gc(ptr addrspace(1) %obj) gc "statepoint-example" {
%statepoint_token = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 2882400000, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) [ "gc-live"(ptr addrspace(1) %obj) ]
%obj.relocated = call coldcc ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %statepoint_token, i32 0, i32 0) ; (%obj, %obj)
ret ptr addrspace(1) %obj.relocated
}
在上面的範例中,指標上的 addrspace(1) 標記是 statepoint-example
GC 策略用於區分引用和非引用的機制。 這是通過 GCStrategy::isGCManagedPointer 控制的。 statepoint-example
和 coreclr
策略(唯一支持安全點的兩個默認策略)都使用 addrspace(1) 來確定哪些指標是引用,但是自定義策略不必遵循此約定。
此傳遞可以作為一種實用函式,供不想在建構 IR 時手動推理活動性、基指標或重新定位的語言前端使用。按照目前的實現,RewriteStatepointsForGC 必須在 SSA 建構(即 mem2ref)之後運行。
RewriteStatepointsForGC 將確保為建立的每個重新定位列出適當的基指標。 它將通過根據需要複製程式碼來做到這一點,以將與每個被重新定位的指標相關聯的基指標傳播到適當的安全點。 該實現假設以下 IR 構造產生基指標:從堆載入、全域變數的地址、函數參數、函數返回值。 常量指標(例如 null)也被假定為基指標。 實際上,可以將此約束放鬆為產生內部派生指標,前提是目標收集器可以從任意內部派生指標找到關聯的分配。
預設情況下,RewriteStatepointsForGC 會將 0xABCDEF00
作為狀態點 ID,將 0
作為可修補位元組數傳遞給新構建的 gc.statepoint
。可以使用屬性 "statepoint-id"
和 "statepoint-num-patch-bytes"
在每個呼叫站點上配置這些值。如果呼叫站點標記有 "statepoint-id"
函式屬性,並且其值為正整數(以字串表示),則該值將用作新構建的 gc.statepoint
的 ID。如果呼叫站點標記有 "statepoint-num-patch-bytes"
函式屬性,並且其值為正整數,則該值將用作新構建的 gc.statepoint
的「可修補位元組數」參數。如果可以成功解析 "statepoint-id"
和 "statepoint-num-patch-bytes"
屬性,則不會將它們傳播到 gc.statepoint
呼叫或調用。
實際上,RewriteStatepointsForGC 應該在傳遞管線中更晚的地方執行,在完成大部分優化之後。這有助於在使用垃圾回收支援進行編譯時提高生成代碼的品質。
RewriteStatepointsForGC 內聯函數降級¶
作為降級到顯式重定位模型的一部分,RewriteStatepointsForGC 對以下內聯函數執行 GC 特定的降級
gc.get.pointer.base
gc.get.pointer.offset
llvm.memcpy.element.unordered.atomic.*
llvm.memmove.element.unordered.atomic.*
memcpy 和 memmove 操作有兩種可能的降級:GC 葉子降級和 GC 可解析降級。如果呼叫明確標記為「gc-leaf-function」屬性,則該呼叫會降級為對「__llvm_memcpy_element_unordered_atomic_*
」或「__llvm_memmove_element_unordered_atomic_*
」符號的 GC 葉子呼叫。此類呼叫無法採取安全點。否則,通過將呼叫包裝到狀態點中,可以使呼叫成為 GC 可解析的。這使得在複製操作期間採取安全點成為可能。請注意,不需要採取安全點即可執行 GC 可解析的複製操作。例如,可以執行短複製操作而無需採取安全點。
對「llvm.memcpy.element.unordered.atomic.*
」、「llvm.memmove.element.unordered.atomic.*
」內聯函數的 GC 可解析呼叫會分別降級為對「__llvm_memcpy_element_unordered_atomic_safepoint_*
」、「__llvm_memmove_element_unordered_atomic_safepoint_*
」符號的呼叫。這樣,運行時就可以提供帶有和不帶有安全點的複製操作實現。
GC 可解析降級還涉及調整呼叫的參數。Memcpy 和 memmove 內聯函數將派生指標作為來源和目標參數。如果複製操作採取安全點,則可能需要重新定位底層來源和目標物件。這需要在複製操作中提供相應的基址指標。為了使基址指標可用,RewriteStatepointsForGC 會將派生指標替換為基址指標和偏移量對。例如
declare void @__llvm_memcpy_element_unordered_atomic_safepoint_1(
i8 addrspace(1)* %dest_base, i64 %dest_offset,
i8 addrspace(1)* %src_base, i64 %src_offset,
i64 %length)
PlaceSafepoints¶
PlaceSafepoints 階段會插入足夠的 Safepoint 輪詢,以確保執行中的程式碼能及時檢查 Safepoint 請求。這個階段預計在 RewriteStatepointsForGC 之前執行,因此不會產生完整的重定位序列。
舉例來說,假設輸入 IR 如下:
define void @test() gc "statepoint-example" {
call void @foo()
ret void
}
declare void @do_safepoint()
define void @gc.safepoint_poll() {
call void @do_safepoint()
ret void
}
這個階段會產生以下 IR:
define void @test() gc "statepoint-example" {
call void @do_safepoint()
call void @foo()
ret void
}
在這個例子中,我們添加了一個(無條件的)進入 Safepoint 輪詢。請注意,儘管看起來是這樣,但進入輪詢不一定是多餘的。我們必須知道 foo
和 test
不是相互遞迴的,才能確定輪詢是多餘的。在實務上,您可能希望您的輪詢定義包含某種形式的條件分支。
目前,PlaceSafepoints 可以在方法進入點和迴圈回邊位置插入 Safepoint 輪詢。如果需要,將其擴展到支援返回輪詢將會很直接。
PlaceSafepoints 包含許多優化,以避免在特定位置放置 Safepoint 輪詢,除非在正常情況下需要確保輪詢的及時執行。PlaceSafepoints 不會嘗試確保在最壞情況下(例如嚴重的系統分頁)及時執行輪詢。
Safepoint 輪詢動作的實作是透過在包含的模組中查找名稱為 gc.safepoint_poll
的函式來指定的。這個函式的程式碼會插入到每個所需的輪詢位置。雖然這個方法中的呼叫或調用會被轉換為 gc.statepoints
,但不會執行遞迴輪詢插入。
這個階段對於任何只需要在 Safepoint 支援垃圾回收語義的語言前端都很有用。如果您需要在 Safepoint 取得其他抽象框架資訊(例如,用於反優化或自省),您可以在前端插入 Safepoint 輪詢。如果您遇到後一種情況,請在 llvm-dev 上詢問建議。我們已經做了很多工作來讓這樣的機制在實務上運作良好,但這些工作還沒有記錄在這裡。
支援的架構¶
對 Statepoint 生成的支援需要每個後端都有一些程式碼。目前只支援 Aarch64 和 X86_64。
限制和未完成的想法¶
混合引用和原始指標¶
支援在抽象機器模型中允許使用未受管理的指標指向垃圾回收物件的語言(例如,將物件的指標傳遞給 C 常式)。目前,關於如何處理這個問題的最佳想法是使用內建函式或不透明函式來隱藏引用值和原始指標之間的關係。問題在於,使用 ptrtoint 或 inttoptr 轉換(這在這種情況下很常見)會破壞在從抽象模型降级到明確的物理模型時用於推斷任意引用的基底指標的規則。請注意,直接降级到物理模型的前端在這裡沒有任何問題。
堆疊上的物件¶
如上所述,如果收集器可以在給定堆疊地址的情況下找到堆積映射,則明確的降级支援在堆疊上分配的物件。
缺少的部分是 a) 與抽象機器模型的重寫(RS4GC)整合,以及 b) 支援選擇性地分解堆疊物件,以便不需要為它們提供堆積映射。後者是為了便於與某些收集器整合而需要的。
降级品質和表示法開銷¶
目前已知狀態點降低的效果不佳。從長遠來看,我們希望將狀態點與暫存器分配器整合;但在短期內不太可能實現。我們發現降低的品質相對不重要,因為熱狀態點幾乎都是內聯器錯誤。
有人擔心狀態點表示法會導致某些範例產生大量的 IR,並且會導致記憶體使用量和編譯時間高於預期。目前沒有立即更改的計劃,但未來可能會探索替代模型。
異常邊緣上的重定位¶
目前在 ToT 中,沿著異常路徑的重定位已損壞。特別是,目前沒有辦法在也有重定位的路徑上表示重新拋出。有關更多詳細信息,請參閱此 llvm-dev 討論。
錯誤和增強功能¶
目前已知的錯誤和正在考慮的增強功能可以通過在摘要欄位中對 [Statepoint] 執行bugzilla 搜索來追踪。提交新的錯誤時,請使用此標籤,以便相關人員看到新提交的錯誤。與大多數 LLVM 功能一樣,設計討論在Discourse 論壇上進行,並且應將修補程序發送到llvm-commits 以供審核。