LLVM 中的垃圾回收安全點¶
狀態¶
本文檔描述了 LLVM 中用於支援垃圾回收的一組擴展。到目前為止,這些機制已在商業 Java 實現中得到充分驗證,其中完全重定位收集器已使用它們出貨。仍然可能潛伏著一些錯誤;這些將在下面指出。
它們仍然被列為「實驗性」,以表明不提供跨版本的前向或後向相容性保證。如果您的使用案例需要某種形式的前向相容性保證,請在 llvm-dev 郵件列表中提出問題。
LLVM 仍然支援使用 gcroot
內建函式的保守式垃圾回收支援的替代機制。gcroot
機制在目前主要具有歷史意義,但有一個例外 - 其陰影堆疊的實現在許多語言前端中已成功使用,並且仍然受到支援。
概述 & 核心概念¶
為了回收無用物件,垃圾收集器必須能夠識別執行代碼中包含的任何物件參考,並且根據收集器的不同,可能需要更新它們。收集器並非在代碼的所有點都需要此資訊 - 那會使問題變得更加困難 - 而僅在執行中定義明確的點,稱為「安全點」。對於大多數收集器來說,追蹤每個唯一指標值的至少一個副本就足夠了。但是,對於希望重定位可從執行代碼直接存取的物件的收集器來說,則需要更高的標準。
另一個額外的挑戰是,編譯器可能會計算指向分配範圍之外甚至另一個分配範圍中間的的中間結果(「衍生指標」)。此中間值的最終使用必須產生分配範圍內的位址,但此類「外部衍生指標」可能對收集器可見。鑑於此,垃圾收集器無法安全地依賴位址的執行階段值來指示其關聯的物件。如果垃圾收集器希望移動任何物件,則編譯器必須為每個指標提供一個映射,以指示其分配。
為了簡化收集器與編譯後代碼之間的互動,大多數垃圾收集器都以三個抽象概念來組織:載入屏障、儲存屏障和安全點。
載入屏障是在機器載入指令之後立即執行,但在使用載入值之前執行的代碼片段。根據收集器的不同,可能需要對所有載入、僅對特定類型(在原始原始碼語言中)的載入或完全不需要屏障。
類似地,儲存屏障是在機器儲存指令之前立即執行,但在計算儲存值之後執行的代碼片段。儲存屏障最常見的用途是更新世代垃圾收集器中的「卡片表」。
安全點是指在該位置,對編譯後代碼可見的指標(即目前在暫存器或堆疊中)可以更改。在安全點完成後,實際指標值可能會有所不同,但指標指向的「物件」(原始碼語言所見)將不會改變。
請注意,「安全點」一詞在某種程度上被過度使用了。它既指機器狀態可解析的位置,也指將應用程式執行緒帶到收集器可以安全地使用該資訊的點所涉及的協調協議。本文檔中使用的術語「狀態點」專指前者。
本文檔重點介紹最後一項 - 編譯器對產生的代碼中安全點的支援。我們將假設外部機制已決定在何處放置安全點。從我們的角度來看,所有安全點都將是函式呼叫。為了支援直接從編譯後代碼中的值重定位物件,收集器必須能夠
在安全點識別指標的每個副本(包括編譯器本身引入的副本),
識別每個指標與哪個物件相關,以及
可能更新每個副本。
本文檔描述了基於 LLVM 的編譯器可以將此資訊提供給語言執行階段/收集器,並確保可以讀取和更新所有指標的機制(如果需要)。
抽象機器模型¶
在高層次上,LLVM 已擴展為支援編譯到抽象機器,該抽象機器使用非整數指標類型擴展了實際目標,該類型適用於表示對物件的垃圾回收參考。特別是,此類非整數指標類型沒有定義到整數表示的映射。這種語義上的怪異之處允許執行階段為程式中的每個點選擇整數映射,從而允許物件的重定位而沒有可見的影響。
這種高層次的抽象機器模型用於大多數最佳化器。因此,轉換 Pass 不需要擴展以查看顯式的重定位序列。在開始代碼生成之前,我們將表示形式切換為顯式形式。選擇用於降低的確切位置是一個實現細節。
請注意,抽象機器模型的大部分價值來自於需要對可能可重定位的物件進行建模的收集器。對於僅支援非重定位收集器的編譯器,您可能希望考慮從完全顯式的形式開始。
警告:目前在非整數指標的定義中存在一個已知的語義漏洞,但尚未在上游解決。為了解決這個問題,您需要停用載入的推測,除非記憶體類型(非整數指標與任何其他類型)已知為未更改。也就是說,如果推測載入導致非整數指標值被載入為任何其他類型或反之亦然,則進行推測載入是不安全的。實際上,此限制已很好地隔離在 ValueTracking.cpp 中的 isSafeToSpeculate 中。
顯式表示¶
前端可以直接產生這種低階顯式形式,但這樣做可能會抑制最佳化。相反,建議使用重定位收集器的編譯器以剛才描述的抽象機器模型為目標。
顯式方法的核心是以一種方式建構(或重寫)IR,使垃圾收集器執行的可能更新在 IR 中顯式可見。這樣做需要我們
為每個可能重定位的指標建立一個新的 SSA 值,並確保在安全點之後無法存取原始(非重定位)值的任何使用,
以對編譯器不透明的方式指定重定位,以確保最佳化器不會在狀態點之後引入非重定位值的新使用。這可以防止最佳化器執行不健全的最佳化。
記錄每個狀態點的活動指標(及其關聯的分配)的映射。
在最抽象的層次上,插入安全點可以被認為是將呼叫指令替換為對多個傳回值函式的呼叫,該函式既呼叫原始呼叫目標,傳回其結果,又傳回垃圾回收物件的任何活動指標的更新值。
請注意,識別垃圾回收值的所有活動指標、轉換 IR 以公開為每個此類活動指標提供基底物件的指標以及正確插入所有內建函式的任務,明確超出了本文檔的範圍。建議的方法是使用下面描述的 實用工具 Pass。
此抽象函式呼叫以一系列內建函式呼叫具體表示,這些內建函式呼叫統稱為「狀態點重定位序列」。
讓我們考慮一下 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 不容易支援這種表示形式。
相反,statepoint 內建函式標記了安全點或狀態點的實際位置。狀態點傳回一個 Token 值(僅在編譯時存在)。為了取回呼叫的原始傳回值,我們使用 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
每個可能重定位的值都已溢出到堆疊,並且該位置的記錄已記錄到 堆疊映射區段。如果垃圾收集器需要在呼叫期間更新任何這些指標,它會確切地知道要更改什麼。
我們的範例的 StackMap 區段的相關部分是
# 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 實用工具 Pass 的測試。因此,可以使用以下命令輕鬆檢查其完整的 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 指標列表中的 allocas。Allocas 可以列出,帶有或不帶有額外的顯式 gc 指標值和重定位。
狀態點運算元列表的 gc 區域中的 alloca 將導致堆疊區域的位址列在狀態點的堆疊映射中。
如果需要,可以使用此機制來描述顯式溢出槽。然後,產生器的責任是確保在安全點的任一側根據需要將值溢出/填充到/從 alloca。請注意,沒有辦法為此類顯式指定的溢出槽指示相應的基底指標,因此使用僅限於關聯收集器可以從指標本身導出物件基底的值。
此機制可用於描述包含參考的堆疊上的物件,前提是收集器可以從堆疊上的位置映射到描述收集器需要處理的參考的內部佈局的堆積映射。
警告:目前,這種替代形式尚未得到充分實踐。建議謹慎使用,並預期必須修復一些錯誤。特別是,RewriteStatepointsForGC 實用工具 Pass 目前對 allocas 不執行任何操作。
基底 & 衍生指標¶
「基底指標」是指向分配(物件)起始位址的指標。「衍生指標」是從基底指標偏移一定量的指標。在重定位物件時,垃圾收集器需要能夠將與分配關聯的每個衍生指標重定位到與新位址相同的偏移量。
「內部衍生指標」保留在它們關聯的分配範圍內。因此,只要執行階段系統知道分配的範圍,就可以在執行階段找到基底物件。
「外部衍生指標」超出關聯物件的範圍;它們甚至可能落在另一個分配位址範圍內。因此,垃圾收集器無法在執行階段確定它們與哪個分配相關聯,並且需要編譯器支援。
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 編譯器將解析並丟棄此格式;它的記憶體效率不高。如果您需要替代格式(例如,用於提前編譯器),請參閱下面 未完成的工作項目 <OpenWork> 下的討論。
每個狀態點生成以下位置
描述呼叫目標的呼叫約定的常數。此常數是用於生成堆疊映射的 LLVM 版本的有效 呼叫約定識別碼。對於此常數,除了 LLVM 在其他地方針對這些識別碼提供的內容之外,不提供額外的相容性保證。
描述傳遞給狀態點內建函式的標誌的常數
描述以下 deopt 位置(非運算元)數量的常數。如果未提供「deopt」捆綁包,則將為 0。
可變數量的位置,每個位置對應於「deopt」運算元捆綁包中列出的每個 deopt 參數。目前,僅支援位寬為 64 位或更少的 deopt 參數。只有在以下情況下,才能指定和報告大於 64 位類型的數值:a) 該值在呼叫站點是常數,並且 b) 假設零擴展到原始位寬,該常數可以用少於 64 位表示。
可變數量的重定位記錄,每個記錄恰好由兩個位置組成。重定位記錄在下面詳細描述。
每個重定位記錄都為收集器提供了足夠的資訊,以重定位一個或多個衍生指標。每個記錄都由一對位置組成。記錄中的第二個元素表示需要更新的指標(或指標)。記錄中的第一個元素提供指向與正在重定位的指標關聯的物件的基底的指標。由於指標可能超出原始分配的範圍,但仍然需要與分配一起重定位,因此處理廣義衍生指標需要此資訊。此外
保證如果基底指標在狀態點之後使用,則基底指標也必須顯式地顯示為重定位對。
重定位記錄可能少於 IR 狀態點中的 gc 參數。每個唯一對將至少出現一次;重複是可能的。
每個記錄中的位置可以是指標大小,也可以是指標大小的倍數。在後一種情況下,記錄必須解釋為描述一系列指標及其對應的基底指標。如果位置的大小為 N x sizeof(指標),則位置中將包含 N 個每個指標的記錄。可以假定一對中的兩個位置的大小相同。
請注意,每個區段中使用的位置可能描述相同的物理位置。例如,堆疊槽可能顯示為 deopt 位置、gc 基底指標和 gc 衍生指標。
對於狀態點記錄,StkMapRecord 的 LiveOut 區段將為空。
安全點語義 & 驗證¶
對於編譯後代碼的正確性(關於垃圾收集器)的基本正確性屬性是動態的。必須是這樣的情況,即不存在動態追蹤,使得涉及可能重定位指標的操作在可能重定位它的安全點之後是可觀察的。「可觀察之後」的此用法意味著外部觀察者可以觀察到此事件序列,從而排除在安全點之前執行操作的可能性。
為了理解為什麼需要此「可觀察之後」屬性,請考慮對重定位指標的原始副本執行的空值比較。假設控制流程跟隨安全點,則無法從外部觀察到空值比較是在安全點之前還是之後執行。(請記住,原始值未被安全點修改。)編譯器可以自由地做出任一調度選擇。
實現的實際正確性屬性比這稍微強一些。我們要求在靜態路徑上,可能重定位的指標在可能已重定位之後不是「可觀察的」。這比嚴格必要的稍微強一些(因此可能會禁止某些原本有效的程式),但極大地簡化了對編譯後代碼正確性的推理。
通過建構,如果正確地在原始碼 IR 中建立此屬性,則最佳化器將保持此屬性。這是設計的關鍵不變性。
現有的 IR 驗證器 Pass 已擴展為檢查其各自文檔中提到的大多數關於內建函式的本地限制。LLVM 中的目前實現不檢查關鍵的重定位不變性,但這是正在進行的開發此類驗證器的工作。如果您有興趣嘗試目前版本,請在 llvm-dev 上詢問。
安全點插入的實用工具 Pass¶
RewriteStatepointsForGC¶
RewriteStatepointsForGC Pass 轉換函式的 IR,以從上面描述的抽象機器模型降低到顯式狀態點重定位模型。為此,它將所有可能包含安全點輪詢的函式的呼叫或調用替換為 gc.statepoint
和相關聯的完整重定位序列,包括所有必需的 gc.relocates
。
此 Pass 僅適用於設定了 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
}
該 Pass 將產生此 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 時手動推理存活性、基底指標或重定位的語言前端,此 Pass 可以用作實用工具函式。按照目前的實現,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
的「num patch bytes」參數。如果 "statepoint-id"
和 "statepoint-num-patch-bytes"
屬性可以成功解析,則不會將其傳播到 gc.statepoint
呼叫或調用。
實際上,RewriteStatepointsForGC 應該在 Pass 管線的後期運行,在大多數最佳化已經完成之後。這有助於提高在使用垃圾回收支援進行編譯時產生的代碼的品質。
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 Pass 插入足夠的安全點輪詢,以確保運行代碼及時檢查安全點請求。預計此 Pass 在 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
}
此 Pass 將產生以下 IR
define void @test() gc "statepoint-example" {
call void @do_safepoint()
call void @foo()
ret void
}
在這種情況下,我們添加了一個(無條件)入口安全點輪詢。請注意,儘管外觀如此,但入口輪詢不一定是多餘的。我們必須知道 foo
和 test
不是互遞歸的,輪詢才是多餘的。實際上,您可能希望您的輪詢定義包含某種形式的條件分支。
目前,PlaceSafepoints 可以在方法入口和迴圈回邊位置插入安全點輪詢。如果需要,將其擴展以與傳回輪詢一起使用將是直接的。
PlaceSafepoints 包括許多最佳化,以避免在特定站點放置安全點輪詢,除非需要確保在正常條件下及時執行輪詢。PlaceSafepoints 不嘗試確保在最壞情況下(例如繁重的系統分頁)及時執行輪詢。
安全點輪詢動作的實作方式,是透過在包含的模組中查找名為 gc.safepoint_poll
的函式來指定。此函式的主體會被插入到每個所需的輪詢點。雖然在此方法內部的呼叫或調用會轉換為 gc.statepoints
,但不會執行遞迴輪詢插入。
此趟 (pass) 適用於任何前端語言,這些語言僅需在安全點支援垃圾收集語意。如果您需要在安全點取得其他抽象框架資訊(例如,用於反最佳化或內省),您可以在前端插入安全點輪詢。如果您屬於後者情況,請在 llvm-dev 上尋求建議。為了使這種方案在實務中良好運作,已經做了大量工作,但尚未在此處記錄。
支援的架構¶
對 statepoint 生成的支援需要一些後端程式碼。目前,僅支援 Aarch64 和 X86_64。
限制與半生不熟的想法¶
混合參考與原始指標¶
在抽象機器模型中,支援允許非託管指標指向垃圾收集物件的語言(即將指向物件的指標傳遞給 C 常式)。目前,關於如何處理這個問題的最佳想法,是使用一個內建函數 (intrinsic) 或不透明函數 (opaque function) 來隱藏參考值和原始指標之間的關聯。問題在於,當從抽象模型降低 (lowering) 到顯式物理模型時,使用 ptrtoint 或 inttoptr 轉換(在這種使用案例中很常見)會破壞用於推斷任意參考之基底指標的規則。請注意,直接降低到物理模型的前端在此處沒有任何問題。
堆疊上的物件¶
如上所述,顯式降低支援在堆疊上分配的物件,前提是收集器可以根據堆疊位址找到堆積映射 (heap map)。
缺少的環節是 a) 從抽象機器模型與重寫 (RS4GC) 整合,以及 b) 支援選擇性地分解堆疊物件,以便不需要它們的堆積映射。後者是為了方便與某些收集器整合而需要的。
降低品質與表示法額外負擔¶
目前已知 statepoint 降低的品質有些差。從長遠來看,我們希望將 statepoint 與暫存器分配器整合;在近期內,這不太可能發生。我們發現降低的品質相對不重要,因為熱點 statepoint 幾乎總是內聯錯誤 (inliner bugs)。
有人提出擔憂,認為 statepoint 表示法會為某些範例產生大量的 IR,並且這會導致高於預期的記憶體使用量和編譯時間。目前沒有立即的計畫要對此進行更改,但在未來可能會探索替代模型。
沿著異常邊緣的重定位¶
目前在 ToT 中,沿著異常路徑的重定位已損壞 (broken)。特別是,目前沒有辦法表示在也具有重定位的路徑上重新拋出 (rethrow)。有關更多詳細資訊,請參閱此 llvm-dev 討論。
錯誤與增強功能¶
目前已知的錯誤和正在考慮的增強功能,可以透過在摘要欄位中執行 [Statepoint] 的 bugzilla 搜尋來追蹤。當提交新的錯誤報告時,請使用此標籤,以便相關方看到新提交的錯誤。與大多數 LLVM 功能一樣,設計討論會在 Discourse 論壇上進行,而修補程式應發送到 llvm-commits 進行審查。