Go 官方詳解“Green Tea”垃圾回收器:從對象到頁,一場應對現(xiàn)代硬件挑戰(zhàn)的架構(gòu)演進
為了幫助大家深入理解這一重大變更背后的技術原理與深層思考,我翻譯了 Go 官方博客10月29日的最新文章《The Green Tea Garbage Collector》。該文是基于 Go 團隊核心成員 Michael Knyszek 在 GopherCon 2025 大會上的演講整理而成。在這篇極具技術深度的原理文章中,沒有人能比官方團隊的講解更為專業(yè)和權威。因此,為了最大程度地保留其“原汁原味”,我選擇以全文翻譯的形式,將其最真實、最精確的面貌呈現(xiàn)給大家。
以下是譯文全文,供大家參考。
Go 1.25 包含一個名為“綠茶”(Green Tea)的全新實驗性垃圾回收器,在構(gòu)建時通過設置 GOEXPERIMENT=greenteagc 即可啟用。使用該垃圾回收器后,許多工作負載在垃圾回收上花費的時間減少了約 10%,而有些工作負載的降幅甚至高達 40%!
它已為生產(chǎn)環(huán)境準備就緒,并在 Google 內(nèi)部投入使用,因此我們鼓勵你進行嘗試。我們知道某些工作負載的收益不大,甚至完全沒有,所以你的反饋對于我們向前推進至關重要。根據(jù)我們目前掌握的數(shù)據(jù),我們計劃在 Go 1.26 中將其設為默認GC。
如需報告任何問題,請?zhí)峤灰粋€新 issue。
如需分享任何成功經(jīng)驗,請回復至現(xiàn)有的 Green Tea issue。
下文是基于 Michael Knyszek 在 GopherCon 2025 上的演講整理的博文。一旦演講視頻上線,我們將會更新此博文并附上鏈接。
追蹤垃圾回收過程
在討論“綠茶”之前,讓我們先就垃圾收集問題達成共識。
對象和指針
垃圾回收的目的是自動回收并重用程序不再使用的內(nèi)存。
為此,Go 垃圾回收器關注的是對象(Object)和指針(Pointer)。
在 Go 運行時的上下文中,對象是Go值(Value),其底層內(nèi)存分配自堆。當 Go 編譯器無法找到其他方式為某個值分配內(nèi)存時,就會創(chuàng)建堆對象。例如,以下代碼片段會分配一個堆對象:一個指針切片的底層存儲空間。
var x = make([]*int, 10) // 全局變量Go 編譯器只能在堆上分配切片后備存儲,因為它很難(甚至可能不可能)知道 x 將引用該對象多長時間。
指針只是一些數(shù)字,用于指示 Go 值在內(nèi)存中的位置,Go 程序通過它們來引用對象。例如,要獲取上一個代碼片段中分配的對象的起始指針,我們可以這樣寫:
&x[0] // 0xc000104000標記-清除算法
Go 的垃圾回收器遵循一種廣義上稱為“追蹤式垃圾回收”的策略,這意味著垃圾回收器會跟隨或追蹤程序中的指針,以識別程序仍在使用的對象。
更具體地說,Go 垃圾回收器實現(xiàn)了標記-清除(mark-sweep)算法。這比聽起來要簡單得多。 可以把對象和指針想象成計算機科學意義上的圖:對象是節(jié)點,指針是邊。
標記-清除算法就在這個圖上運行的,顧名思義,它分兩個階段進行。
在第一階段,即標記階段,它從一組明確定義的、稱為“根(root)”的源邊開始遍歷對象圖??梢詫⑵淅斫鉃槿肿兞亢途植孔兞俊H缓?,它將沿途找到的所有東西標記為已訪問(visited),以避免循環(huán)。這類似于典型的圖遍歷算法,如深度優(yōu)先或廣度優(yōu)先搜索。
接下來是清除階段。在我們的圖遍歷中未被訪問到的任何對象,都是程序未使用或不可達(unreachable)的。我們稱這種狀態(tài)為不可達,因為通過語言的語義,正常的安全 Go 代碼已無法再訪問那塊內(nèi)存。為完成清除階段,算法只需遍歷所有未訪問的節(jié)點,并將其內(nèi)存標記為空閑,以便內(nèi)存分配器可以重用它們。
就是這樣?
你可能覺得我在這里把事情想得有點過于簡單了。垃圾回收器經(jīng)常被比作魔法和黑盒子 。你的說法也對了一部分,實際情況要復雜得多。
例如,實際上,這個算法會與你的常規(guī) Go 代碼并行執(zhí)行。遍歷一個不斷變化的圖會帶來挑戰(zhàn)。我們還對這個算法進行了并行化,這一點稍后會再次提及。
但請相信我,這些細節(jié)大多與核心算法無關。核心算法實際上只是一個簡單的圖泛洪(graph flood)操作。
圖泛洪示例
我們來看一個例子。請瀏覽下面的幻燈片圖片,跟隨步驟操作。
圖片
這里我們有一個包含一些全局變量和 Go 堆的圖示。讓我們一步步來分析。
圖片
左邊是我們的根。它們是全局變量 x 和 y。這將是我們圖遍歷的起點。根據(jù)左下角的圖例,它們被標記為藍色,表示它們當前在我們的工作列表上。
圖片
右邊是我們的堆。目前,堆中的所有東西都是灰色的,因為我們還沒有訪問過任何部分。
圖片
每個矩形中代表一個對象。每個對象都標有其類型。這個特殊的對象是 T 類型的對象,其類型定義在左上角。它有一個指向子節(jié)點數(shù)組的指針和一些值。我們可以推斷這是一種遞歸的樹形數(shù)據(jù)結(jié)構(gòu)。
圖片
除了 T 類型的對象,你還會注意到我們有包含 *T 的數(shù)組對象。這些數(shù)組對象由 T 類型對象的 "children" 字段指向。
圖片
矩形內(nèi)的每個方塊代表 8 字節(jié)的內(nèi)存。帶有點的方塊是一個指針。如果它有箭頭,那么它是一個指向某個其他對象的非空指針。
圖片
如果它沒有對應的箭頭,那么它就是一個空指針。
圖片
接下來,這些虛線矩形代表空閑空間,我稱之為空閑“槽位(slot)”。我們可以在那里放置一個對象,但目前還沒有。
圖片
你還會注意到對象被這些帶標簽的、虛線圓角矩形組合在一起。每一個都代表一個頁(page):一塊連續(xù)的內(nèi)存塊。這些頁被標記為 A、B、C 和 D,我將以此來稱呼它們。
在這個圖中,每個對象都被分配到某個頁面中。就像實際實現(xiàn)一樣,這里的每個頁面只包含特定大小的對象。這正是 Go 堆的組織方式。
圖片
頁也是我們組織每個對象元數(shù)據(jù)的方式。這里你可以看到七個框,每個對應頁 A 中的七個對象槽位之一。
每個框代表一位(bit)信息:我們之前是否見過這個對象。實際上,Go運行時就是通過這種方式來管理對象是否已被訪問過的,這一點稍后會很重要。
圖片
細節(jié)講了很多,感謝你跟讀。這些稍后都會派上用場?,F(xiàn)在,讓我們看看圖泛洪如何應用于這幅圖。
圖片
我們首先從工作列表中取出一個根。我們將其標記為紅色,表示它現(xiàn)在是活躍的。
圖片
沿著根指針,我們找到了一個 T 類型的對象,并將其添加到我們的工作列表。根據(jù)圖例,我們將該對象繪制成藍色,以表明它已在工作列表中。請注意,我們同時在右上角的元數(shù)據(jù)中設置了與此對象對應的“已見”位。
圖片
下一個根也同樣處理。
圖片
現(xiàn)在我們處理完了所有的根,工作列表上還剩下兩個對象。讓我們從工作列表中取出一個對象。
圖片
我們現(xiàn)在要做的是遍歷該對象的指針,以找到更多的對象。順便說一下,我們稱遍歷一個對象的指針為“掃描”該對象。
圖片
我們找到了這個有效的數(shù)組對象…
圖片
… 并將其添加到我們的工作列表中。
圖片
從這里開始,我們遞歸地進行。
圖片
我們遍歷數(shù)組的指針。
圖片
圖片
圖片
找到更多對象…
圖片
圖片
圖片
然后我們遍歷數(shù)組對象引用的那些對象!
圖片
請注意,我們?nèi)匀恍枰闅v所有指針,即使它們是 nil。我們事先并不知道它們是否為空。
圖片
這個分支下還有一個對象…
圖片
圖片
現(xiàn)在我們到達了另一個分支,從我們早先從某個根找到的頁 A 中的那個對象開始。
你可能注意到了我們工作列表的“后進先出”規(guī)則,這表明我們的工作列表是一個棧,因此我們的圖遍歷近似于深度優(yōu)先。這是有意為之的,并反映了 Go 運行時中實際的圖遍歷算法。
圖片
讓我們繼續(xù)…
圖片
接下來我們找到了另一個數(shù)組對象…
圖片
并遍歷它…
圖片
圖片
圖片
圖片
圖片
我們的工作列表上只剩最后一個對象了…
圖片
讓我們掃描它…
圖片
圖片
標記階段完成了!我們沒有任何正在處理的工作,工作列表也空了。所有用黑色繪制的對象都是可達的,所有用灰色繪制的對象都是不可達的。讓我們一次性清除所有不可達的對象。
圖片
我們已將那些對象轉(zhuǎn)換為空閑槽位,準備好容納新的對象。
問題所在
經(jīng)過上面一番摸索,我認為我們已經(jīng)掌握了 Go 垃圾回收器的實際工作原理。目前看來,這個過程運行良好,那么問題出在哪里呢?
事實證明,在某些程序中,執(zhí)行這個特定算法會花費大量時間,而且?guī)缀鯐o所有 Go 程序帶來顯著的開銷。Go 程序?qū)?20% 甚至更多的 CPU 時間用于垃圾回收的情況并不少見。
讓我們來分析一下這些時間都花在了哪里。
垃圾回收成本
在宏觀層面上,垃圾回收器的成本由兩部分組成。一是運行頻率,二是每次運行所做的工作量。將這兩者相乘,就得到了垃圾回收的總成本。
Total GC cost = Number of GC cycles × Average cost per GC cycle
即 總 GC 成本 = GC 周期數(shù) × 每個 GC 周期的平均成本多年來,我們一直在研究這個等式中的這兩個術語。要了解更多關于垃圾回收器運行頻率的信息,請參閱 Michael 在 2022 年 GopherCon EU 大會上的關于內(nèi)存限制的演講。 Go 垃圾回收器的指南也對此主題進行了很多闡述,如果你想深入了解,值得一看。
但現(xiàn)在,我們只關注第二部分,即每個周期的成本。
多年來,我們不斷研究 CPU Profile分析結(jié)果,試圖提高性能,從中我們了解到 Go 的垃圾回收器有兩大特點。
第一,大約 90% 的垃圾回收器成本都花在了標記上,只有大約 10% 是在清除。事實證明,清除比標記更容易優(yōu)化,多年來 Go 已經(jīng)擁有了一個非常高效的清除器。
第二,在那段用于標記的時間里,有相當大一部分(通常至少有 35%),都浪費在了訪問堆內(nèi)存上。這本身已經(jīng)夠糟糕了,更糟糕的是,它完全阻礙了現(xiàn)代 CPU 真正高速運行的關鍵機制。
“微架構(gòu)災難”
在這種情況下,“堵塞工作機制(gump up the works)”意味著什么?現(xiàn)代 CPU 的具體構(gòu)造相當復雜,所以我們用一個類比來說明。
想象 CPU 在一條路上行駛,這條路就是你的程序。CPU 想要加速到很高的速度,為此它需要能看清前方的路,并且道路必須暢通。但圖遍歷算法對 CPU 來說,就像在城市街道里開車。CPU 看不到拐角后的情況,也無法預測接下來會發(fā)生什么。為了前進,它必須不斷地減速、轉(zhuǎn)彎、在紅綠燈前停下、避開行人。你的引擎有多快幾乎無關緊要,因為你根本沒有機會真正跑起來。
讓我們通過再次審視我們的例子來使這一點更具體。我在這里的堆上疊加了我們所走的路徑。每個從左到右的箭頭代表我們做的一段掃描工作,虛線箭頭則顯示了我們在不同掃描工作之間是如何跳轉(zhuǎn)的。
圖片
上圖展示了我們的圖泛洪示例中,垃圾回收器在堆中執(zhí)行的路徑。
請注意,我們正在內(nèi)存中到處跳轉(zhuǎn),在每個地方只做一點點工作。特別是,我們頻繁地在頁之間,以及頁的不同部分之間跳轉(zhuǎn)。
現(xiàn)代 CPU 做了大量的緩存。訪問主內(nèi)存可能比訪問緩存中的內(nèi)存慢上 100 倍。CPU 緩存中填充的是最近訪問過的內(nèi)存,以及與最近訪問過的內(nèi)存相鄰的內(nèi)存。但是,并不能保證兩個相互指向的對象在內(nèi)存中也彼此靠近。圖泛洪算法并沒有考慮到這一點。
補充一點:如果我們只是在等待從主內(nèi)存中獲取數(shù)據(jù),情況可能還沒那么糟。CPU 會異步地發(fā)出內(nèi)存請求,所以即使是慢的請求也可以重疊,只要 CPU 能看得足夠遠。但在圖遍歷中,每一小段工作都是不可預測的,并且高度依賴于上一段工作,所以 CPU 被迫幾乎在每一次獨立的內(nèi)存獲取后都進行等待。
不幸的是,對我們來說,這個問題只會越來越嚴重。業(yè)界有句格言:“等兩年,你的代碼會變得更快?!?/p>
但 Go,作為一個依賴于標記-清除算法的垃圾回收語言,卻面臨著相反的風險。“等兩年,你的代碼會變得更慢?!?現(xiàn)代 CPU 硬件的趨勢正在給垃圾回收器的性能帶來新的挑戰(zhàn):
- 非一致性內(nèi)存訪問 (Non-uniform memory access)。 首先,內(nèi)存現(xiàn)在往往與 CPU 核心的子集相關聯(lián)。其他 CPU 核心訪問該內(nèi)存的速度比前者慢。換句話說,主內(nèi)存訪問的成本取決于哪個 CPU 核心正在訪問它。這種成本是不一致的,因此我們稱之為非一致內(nèi)存訪問,簡稱 NUMA。
- 內(nèi)存帶寬減少 (Reduced memory bandwidth)。 每個 CPU 的可用內(nèi)存帶寬隨著時間推移呈下降趨勢。這意味著雖然我們擁有更多的 CPU 核心,但每個核心能夠提交的數(shù)據(jù)量相對較少。 對主內(nèi)存的請求導致未緩存的請求等待時間比以前更長。
- 越來越多的 CPU 核心 (Ever more CPU cores)。 上面,我們看的是一個順序的標記算法,但真正的垃圾回收器是并行執(zhí)行此算法的。這在核心數(shù)量有限的情況下擴展得很好,但即使經(jīng)過精心設計,用于掃描的共享對象隊列也會成為一個瓶頸。
- 現(xiàn)代硬件特性 (Modern hardware features)。 新硬件擁有像向量指令這樣的酷炫功能,讓我們能一次性操作大量數(shù)據(jù)。雖然這有可能大幅提升速度,但目前還不清楚如何才能實現(xiàn)這一點。因為標記工作包含很多不規(guī)則且通常是小塊的工作。
綠茶(Green Tea)
最后,我們來看看綠茶算法,這是我們對標記掃描算法的一個新的嘗試。綠茶算法的核心思想非常簡單:
操作頁面,而不是對象。
聽起來很簡單,對吧?然而,為了弄清楚如何安排對象圖遍歷的順序以及我們需要跟蹤哪些內(nèi)容才能使其在實踐中有效運作,我們做了大量的工作。
更具體地說,這意味著:
- 我們不再掃描對象,而是掃描整個頁。
- 我們不再在工作列表上跟蹤對象,而是跟蹤整個頁。
- 我們最終(在一個掃描周期結(jié)束時)仍然需要標記對象,但我們會跟蹤每個頁面本地標記的對象,而不是跟蹤整個堆中的標記對象。
綠茶示例
讓我們通過再次審視我們的示例堆,來看看這在實踐中意味著什么,但這次運行的是“綠茶”而不是直接的圖泛洪。
和之前一樣,請跟隨帶注釋的幻燈片進行瀏覽。
圖片
這和之前的堆是一樣的,但現(xiàn)在每個對象有兩個比特的元數(shù)據(jù)而不是一個。同樣,每個比特或框,對應于頁中的一個對象槽位??偟膩碚f,我們現(xiàn)在有 14 個比特對應于頁 A 中的七個槽位。
頂部的比特代表和以前一樣的東西:我們是否見過一個指向該對象的指針。我稱之為“已見” (seen) 位。底部的比特集是新的。這些“已掃描” (scanned) 位跟蹤我們是否已經(jīng)掃描了該對象。
這塊新的元數(shù)據(jù)是必需的,因為在“綠茶”中,工作列表跟蹤的是頁,而不是對象。我們?nèi)匀恍枰谀撤N程度上跟蹤對象,這就是這些比特的目的。
圖片
我們和以前一樣開始,從根開始遍歷對象。
圖片
圖片
但這一次,我們不是把一個對象放到工作列表上,而是把整個頁——在這里是頁 A——放到工作列表上,通過將整個頁用藍色陰影表示。
圖片
我們找到的對象也是藍色的,表示當我們從工作列表中取出這個頁時,我們將需要查看那個對象。請注意,對象的藍色調(diào)直接反映了頁 A 中的元數(shù)據(jù)。其對應的“已見”位被設置,但其“已掃描”位沒有。
圖片
我們跟隨下一個根,找到另一個對象,再次將整個頁——頁 C——放到工作列表上,并設置該對象的“已見”位。
圖片
我們處理完根了,所以我們轉(zhuǎn)向工作列表,并從工作列表中取出頁 A。
圖片
通過“已見”和“已掃描”位,我們可以知道頁 A 上有一個對象需要掃描。
圖片
我們掃描那個對象,跟隨它的指針。結(jié)果,我們將頁 B 添加到工作列表,因為頁 A 中的第一個對象指向了頁 B 中的一個對象。
圖片
我們處理完頁 A 了。接下來我們從工作列表中取出頁 C。
圖片
與頁 A 類似,頁 C 上有一個單獨的對象需要掃描。
圖片
我們在頁 B 中找到了一個指向另一個對象的指針。頁 B 已經(jīng)在工作列表上了,所以我們不需要向工作列表添加任何東西。我們只需為目標對象設置“已見”位。
圖片
現(xiàn)在輪到頁 B 了。我們在頁 B 上累積了兩個待掃描的對象,我們可以按內(nèi)存順序,連續(xù)處理這兩個對象!
圖片
我們遍歷第一個對象的指針…
圖片
圖片
圖片
我們在頁 A 中找到了一個指向一個對象的指針。頁 A 之前在工作列表上,但此時不在了,所以我們把它放回工作列表。與原始的標記-清除算法不同,在原始算法中,任何給定的對象在整個標記階段最多只會被添加到工作列表一次;而在“綠茶”中,一個給定的頁在標記階段可能會多次出現(xiàn)在工作列表上。
圖片
圖片
我們在掃描完第一個之后,立即掃描頁中的第二個“已見”對象。
圖片
圖片
圖片
我們在頁 A 中又找到了幾個對象…
圖片
圖片
圖片
圖片
我們掃描完頁 B 了,所以我們從工作列表中取出頁 A。
圖片
這次我們只需要掃描三個對象,而不是四個,因為我們已經(jīng)掃描過第一個對象了。我們通過查看“已見”和“已掃描”位之間的差異,來知道要掃描哪些對象。
圖片
我們將按順序掃描這些對象。
圖片
圖片
圖片
圖片
圖片
圖片
我們完成了!工作列表上沒有更多的頁了,我們也沒有正在處理的東西。請注意,現(xiàn)在元數(shù)據(jù)都很好地對齊了,因為所有可達的對象都既被“已見”又被“已掃描”。
你可能在我們的遍歷過程中也注意到了,工作列表的順序與圖遍歷有點不同。圖遍歷是“后進先出”或類似棧的順序,而這里我們對工作列表上的頁使用的是“先進先出”或類似隊列的順序。
這是有意為之的。當頁在隊列中等待時,我們讓“已見”對象在每個頁上累積,這樣我們就可以一次性處理盡可能多的對象。這就是我們能一次性處理頁 A 上那么多對象的原因。有時候,懶惰是一種美德。
圖片
最后,我們可以像以前一樣,清除掉未訪問的對象。
駛上高速公路
讓我們回到我們開車的比喻。我們終于要上高速公路了嗎?
讓我們回顧一下之前的圖泛洪圖片。
圖片
原始圖遍歷在堆中穿行的路徑需要 7 次獨立的掃描。
我們到處跳躍,在不同的地方做著零碎的工作?!熬G茶”所走的路徑看起來非常不同。
圖片
“綠茶”所走的路徑僅需要 4 次掃描。
相比之下,綠茶在 A 和 B 頁面上從左到右的移動次數(shù)較少,但每次移動時間更長。 這些箭頭越長越好,箭頭堆積越多,這種效果就越強。這就是綠茶的魅力所在。
這也是我們馳騁高速公路的機會。
這一切都使得它與微架構(gòu)更加契合。現(xiàn)在,我們可以更精確地掃描彼此靠近的對象,從而更有可能利用緩存并避免使用主內(nèi)存。同樣,每頁的元數(shù)據(jù)也更有可能被緩存。跟蹤頁面而非對象意味著工作列表更小,而工作列表壓力的降低意味著爭用更少,CPU 停頓也更少。
說到高速公路,我們可以把我們比喻意義上的引擎開到以前從未開過的檔位,因為現(xiàn)在我們可以使用向量硬件了!
向量加速
如果你對向量硬件只有粗淺的了解,可能會不明白我們在這里如何使用它。但除了常見的算術和三角運算之外,最新的向量硬件還支持兩項對綠茶算法非常有用的功能:超寬寄存器和復雜的位運算。
大多數(shù)現(xiàn)代 x86 CPU 都支持 AVX-512 指令集,它擁有 512 位寬的向量寄存器。如此寬的寄存器足以在 CPU 上僅使用兩個寄存器來存儲整個頁面的所有元數(shù)據(jù),從而使 Green Tea 能夠僅用幾條直線指令就完成整個頁面的掃描。向量硬件長期以來一直支持對整個向量寄存器進行基本的位運算,但從 AMD Zen 4 和 Intel Ice Lake 開始,它還支持一種新的位向量“瑞士軍刀”指令,使得 Green Tea 掃描過程中的關鍵步驟能夠在幾個 CPU 周期內(nèi)完成。這些改進共同作用,使我們能夠大幅提升 Green Tea 的掃描循環(huán)速度。
對于之前的圖泛洪來說,這根本不可能,因為我們需要在各種大小的對象之間來回掃描。有時只需要兩條元數(shù)據(jù),有時卻需要一萬條。向量硬件根本無法滿足這種可預測性和規(guī)律性要求。
如果你想深入了解一些細節(jié),請繼續(xù)閱讀!否則,請隨時跳到下面的【評估】小節(jié)。
AVX-512 掃描內(nèi)核
要了解 AVX-512 GC 掃描是什么樣子,請看下面的圖。
用于掃描的 AVX-512 矢量內(nèi)核
這里面涉及的內(nèi)容很多,我們可能光是解釋它的運作原理就能寫一整篇博客文章?,F(xiàn)在,我們先從宏觀層面來概括一下:
- 首先,我們獲取頁面的“已查看”和“已掃描”位。請記住,頁面中的每個對象對應一位,并且頁面中的所有對象大小相同。
- 接下來,我們比較這兩個位集。它們的并集成為新的“掃描”位,而它們的差集則是“活動對象”位圖,它告訴我們在本次頁面掃描過程中(與之前的掃描相比)需要掃描哪些對象。
- 我們計算兩個位圖的差值并進行“擴展”,這樣就不是每個對象占用一位,而是頁面中的每個字(8 字節(jié))占用一位。我們稱之為“活動字”位圖。例如,如果頁面存儲 6 個字(48 字節(jié))的對象,則活動對象位圖中的每位將被復制到活動字位圖中的 6 位。如下所示:
0 0 1 1 ... → 000000 000000 111111 111111 ...- 接下來,我們獲取頁面的指針/標量位圖。同樣,這里的每一位都對應頁面的一個字(8 字節(jié)),并告訴我們該字是否存儲指針。這些數(shù)據(jù)由內(nèi)存分配器管理。
- 現(xiàn)在,我們?nèi)≈羔?標量位圖和活動字位圖的交集。結(jié)果就是“活動指針位圖”:該位圖告訴我們尚未掃描的任何活動對象中包含的整個頁面中每個指針的位置。
- 最后,我們可以遍歷頁面內(nèi)存并收集所有指針。邏輯上,我們遍歷活動指針位圖中的每個置位,加載該字處的指針值,并將其寫回緩沖區(qū)。該緩沖區(qū)稍后將用于標記已訪問的對象并將頁面添加到工作列表中。利用向量指令,我們只需幾條指令即可一次處理 64 字節(jié)。
讓這一切變快的部分原因是 VGF2P8AFFINEQB 指令,它是“Galios Field新指令” x86 擴展的一部分,也是我們上面提到的位操作“瑞士軍刀”。它是真正的明星,因為它讓我們能夠非常高效地完成掃描內(nèi)核中的第 (3) 步。它執(zhí)行逐位的仿射變換,將向量中的每個字節(jié)本身視為一個 8 位的數(shù)學向量,并將其與一個 8x8 的比特矩陣相乘。這一切都是在Galios Field GF(2) 上完成的,這意味著乘法是AND,加法是XOR。這樣做的好處是,我們可以為每個對象大小定義幾個 8x8 的比特矩陣,來精確地執(zhí)行我們需要的 1:n 比特擴展。
完整的匯編代碼,請看這個文件。“擴展器”為每個大小類別使用不同的矩陣和不同的排列,所以它們在一個由代碼生成器編寫的單獨文件中。除了擴展函數(shù),代碼量其實不多。大部分代碼都被極大地簡化了,因為我們可以在純粹位于寄存器中的數(shù)據(jù)上執(zhí)行大部分上述操作。而且,希望很快這段匯編代碼將被 Go 代碼所取代!
感謝 Austin Clements 設計了這個過程。它非常酷,而且非常快!
評估
那么,這就是Green Tea的工作原理。它到底有多大幫助呢?
效果可能相當顯著。即使不考慮向量增強,我們的基準測試套件也顯示垃圾回收的 CPU 成本降低了 10% 到 40%。例如,如果應用程序 10% 的時間都花在了垃圾回收器上,那么根據(jù)工作負載的具體情況,整體 CPU 消耗將降低 1% 到 4%。垃圾回收 CPU 時間降低 10% 大致是典型的改進幅度。 (有關這些細節(jié),請參閱 GitHub issue。)
我們在谷歌內(nèi)部推廣了綠茶,并且大規(guī)模推廣后也看到了類似的效果。
我們?nèi)栽谕瞥鱿蛄吭鰪姽δ?,但基準測試和早期結(jié)果表明,這將額外帶來 10%的 GC CPU 降低。
雖然大多數(shù)工作負載都能在一定程度上受益,但也有一些工作負載不會受益。
Green Tea 算法基于這樣的假設:我們可以一次性在單頁上累積足夠多的對象進行掃描,從而抵消累積過程的成本。如果堆結(jié)構(gòu)非常規(guī)則(對象大小相同,且在對象圖中的深度也相近),那么這個假設顯然成立。但是,有些工作負載通常要求我們每次只能掃描一個對象。這可能比圖泛洪更糟糕,因為我們可能在嘗試累積對象到頁面上的過程中,反而做了更多工作,最終卻失敗了。
Green Tea 算法針對僅包含單個待掃描對象的頁面進行了特殊處理。這有助于減少性能回退,但并不能完全消除它們。
然而,要超越圖泛洪算法,所需的單頁累積數(shù)據(jù)量遠比你想象的要少。這項研究的一個意外發(fā)現(xiàn)是,每次僅掃描頁面 2% 的數(shù)據(jù)就能取得比圖泛洪算法更好的性能。
可用性
“綠茶”已經(jīng)在最近的 Go 1.25 版本中作為實驗性功能提供,并且可以通過在構(gòu)建時將環(huán)境變量 GOEXPERIMENT 設置為 greenteagc 來啟用。這不包括前述的向量加速。
我們預計在 Go 1.26 中將“綠茶”作為默認的垃圾回收器,但你仍然可以通過 GOEXPERIMENT=nogreenteagc在構(gòu)建時選擇退出。Go 1.26 還將在較新的 x86 硬件上增加向量加速,并根據(jù)我們收集的反饋包含一系列的調(diào)整和改進。
如果可以,我們鼓勵你嘗試使用 Go 的最新tip版本!如果你更喜歡使用 Go 1.25,我們也同樣歡迎您的反饋。請參閱這個 GitHub 評論,其中包含一些關于我們感興趣的診斷信息、如果你可以分享的話,以及首選的反饋渠道的細節(jié)。
旅程
在結(jié)束這篇博文之前,讓我們花點時間談談我們走到今天的歷程,以及這項技術背后的人的因素。
綠茶的核心理念看似簡單,就像某個人靈光一閃的靈感火花。
但事實并非如此。“綠茶”是許多人多年來共同努力和構(gòu)思的成果。Go 團隊的多位成員都參與了構(gòu)思,包括 Michael Pratt、Cherry Mui、David Chase 和 Keith Randall。當時在英特爾工作的 Yves Vandriessche 的微架構(gòu)見解也對設計探索起到了至關重要的作用。為了使這個看似簡單的理念得以實現(xiàn),我們嘗試了許多方法,也處理了許多細節(jié)問題。
圖片
時間線描繪了我們在達到今天這種狀態(tài)之前,嘗試過的一些類似想法。
這個想法的萌芽可以追溯到2018年。有趣的是,團隊里的每個人都認為最初的想法是別人提出的。
綠茶這個名字是在2024年得來的。當時,奧斯汀在日本四處尋覓咖啡館,喝了無數(shù)抹茶,并由此構(gòu)思出了早期版本的原型!這個原型證明了綠茶的核心理念是可行的。從此,我們便開始了綠茶的研發(fā)之路。
在 2025 年,隨著 Michael 將綠茶項目實施并投入生產(chǎn),其理念進一步發(fā)展和變化。
這需要大量的協(xié)作探索,因為綠茶算法不僅僅是一個算法,而是一個完整的設計空間。我們認為,單憑我們中的任何一個人都無法獨自駕馭它。僅僅有想法是不夠的,你還需要弄清楚細節(jié)并加以驗證?,F(xiàn)在我們已經(jīng)做到了,終于可以開始迭代了。
“綠茶”的未來是光明的。
再次,請通過設置 GOEXPERIMENT=greenteagc 來嘗試它,并讓我們知道它的效果如何!我們對這項工作感到非常興奮,并希望聽到你的聲音!





























