2025年10月29日 Go生态洞察:Go 1.25中的Green Tea垃圾回收器:一种革新的解决方案

摘要

大家好,我是猫头虎!今天我为大家带来了关于Go 1.25版本中引入的Green Tea垃圾回收器的详细解析。这是一种全新的实验性垃圾回收器,通过优化垃圾回收的过程,减少了高达40%的CPU时间。本文将深入讨论Green Tea的工作原理,如何通过“基于页面的回收”来提高性能,并且通过详细的图示演示其与传统垃圾回收算法的不同。欢迎大家尝试并反馈使用体验!关键词:Go 1.25, Green Tea, 垃圾回收, 性能优化


引言

Go语言作为现代编程语言之一,以其强大的并发模型和高效的内存管理著称。垃圾回收(GC)一直是Go的一大亮点,它自动管理内存,减轻了开发者的负担。然而,随着程序规模的增大,垃圾回收带来的性能开销也变得日益显著。为了应对这一挑战,Go 1.25版本引入了一个新的实验性垃圾回收器——Green Tea,其通过优化内存扫描过程,大大提升了垃圾回收的效率。

Green Tea垃圾回收器的核心思想非常简单:以页面为单位进行垃圾回收,而不是单个对象。这种策略显著减少了内存访问的跳跃,充分利用了现代CPU的缓存机制,从而加速了回收过程。本文将结合具体示例,深入探讨Green Tea的工作原理,并通过一系列图示,帮助大家更好地理解这一技术的优势。


猫头虎AI分享:Go生态洞察


作者简介

猫头虎是谁?

大家好,我是 猫头虎,猫头虎技术团队创始人,也被大家称为猫哥。我目前是COC北京城市开发者社区主理人COC西安城市开发者社区主理人,以及云原生开发者社区主理人,在多个技术领域如云原生、前端、后端、运维和AI都具备丰富经验。

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用方法、前沿科技资讯、产品评测、产品使用体验,以及产品优缺点分析、横向对比、技术沙龙参会体验等。我的分享聚焦于云服务产品评测、AI产品对比、开发板性能测试和技术报告

目前,我活跃在CSDN、51CTO、腾讯云、阿里云开发者社区、知乎、微信公众号、视频号、抖音、B站、小红书等平台,全网粉丝已超过30万。我所有平台的IP名称统一为猫头虎猫头虎技术团队

我希望通过我的分享,帮助大家更好地掌握和使用各种技术产品,提升开发效率与体验。


作者名片 ✍️

  • 博主猫头虎
  • 全网搜索IP关键词猫头虎
  • 作者微信号Libin9iOak
  • 作者公众号猫头虎技术团队
  • 更新日期2025年07月21日
  • 🌟 欢迎来到猫头虎的博客 — 探索技术的无限可能!

加入我们AI编程共创团队 🌐

加入猫头虎的AI共创编程圈,一起探索编程世界的无限可能! 🚀

在这里插入图片描述


🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁

🦄 博客首页——🐅🐾猫头虎的博客🎐

正文

🐱 垃圾回收概述

在深入Green Tea之前,让我们快速回顾一下Go的垃圾回收机制,特别是它如何处理对象指针

🐯 对象与指针

Go的垃圾回收主要关注的是“对象”和“指针”。对象是从堆内存分配的Go值,它们通常在程序中引用其他对象。在Go中,指针指向对象在内存中的位置。每当创建新的对象时,Go会通过指针来引用它们,从而保证程序能够访问这些对象。

例如,以下代码创建了一个堆对象:

var x = make([]*int, 10) // global

这里,x 是一个切片,它的底层存储是在堆上分配的。Go垃圾回收器需要跟踪这些对象,确保在它们不再被引用时能够释放相应的内存。

🐱 标记-清扫算法

Go的垃圾回收器实现了一个经典的标记-清扫(mark-sweep)算法。该算法将内存中的对象和指针视为一个图,通过追踪指针来标记哪些对象仍然被程序使用。

  1. 标记阶段:垃圾回收器从根对象(如全局变量和局部变量)开始,遍历对象图,标记所有可达的对象。
  2. 清扫阶段:对于未被标记的对象,垃圾回收器认为它们已经不再使用,将它们的内存标记为可用,从而释放掉这些内存。

图1:标记-清扫算法示意图

🐯 标记-清扫的挑战

尽管标记-清扫算法在大多数情况下都能有效工作,但它在执行时常常存在性能问题,特别是在内存访问模式不规则时。垃圾回收器需要频繁地访问内存中的各个位置,导致CPU缓存命中率较低,从而影响程序的执行效率。


🐱 Green Tea的工作原理

Green Tea垃圾回收器的核心思想是:以页面为单位进行标记和扫描。与传统的标记-清扫算法不同,Green Tea不再单独扫描每个对象,而是以整个页面为单位来管理对象的扫描过程。

🐯 Green Tea的关键特点
  • 以页面为单位进行标记:在Green Tea中,垃圾回收器不再像传统算法那样逐个处理对象,而是处理整个内存页面。这种做法极大地减少了内存访问的跳跃,提高了缓存命中率。
  • 页面级工作列表:Green Tea将工作列表中的元素从单个对象切换为内存页面,这样可以一次性处理多个对象。
  • 本地标记:每个页面内的对象都会有本地标记,表示它们是否已被扫描。这种做法避免了全局范围内的复杂标记操作。

追踪垃圾回收过程#tracing-garbage-collection)

在讨论绿茶之前,让我们先就垃圾收集问题达成共识。

对象和指针#objects-and-pointers)

垃圾回收的目的是自动回收并重新利用程序不再使用的内存。

为此,Go 垃圾回收器主要关注对象指针

在 Go 运行时环境中,对象是 Go 值,其底层内存分配自堆。当 Go 编译器无法确定如何为某个值分配内存时,就会创建堆对象。例如,以下代码片段分配了一个堆对象:一个指针切片的底层存储。

var x = make([]*int, 10) // global

Go 编译器只能在堆上分配切片后备存储,因为它很难(甚至可能不可能)知道x它将引用该对象多长时间。

指针只是指示 Go 值在内存中位置的数字,Go 程序就是通过指针来引用对象的。例如,要获取指向上一段代码中分配的对象的起始位置的指针,我们可以这样写:

&x[0] // 0xc000104000

标记扫描算法#the-mark-sweep-algorithm)

Go 的垃圾回收器遵循一种被称为追踪垃圾回收的策略,这意味着垃圾回收器会跟踪程序中的指针,以确定程序仍在使用的哪些对象。

更具体地说,Go 的垃圾回收器实现了标记清除算法。这比听起来要简单得多。可以把对象和指针想象成计算机科学意义上的图。对象是节点,指针是边。

标记扫描算法在此图上运行,顾名思义,它分两个阶段进行。

在第一阶段,即标记阶段,算法从定义明确的源边(称为根边)开始遍历对象图。可以将其理解为全局变量和局部变量。然后,它将沿途遇到的所有对象标记为已**访问,以避免循环往复。这类似于典型的图洪水算法,例如深度优先搜索或广度优先搜索。

接下来是清扫阶段。在图遍历过程中未被访问的对象都处于未使用状态,或者说程序无法访问。之所以称之为不可达状态,是因为根据语言语义,使用普通的安全 Go 代码已经无法再访问这些内存。为了完成清扫阶段,算法会遍历所有未访问的节点,并将它们的内存标记为空闲,以便内存分配器可以重用这些内存。

就是这样?#thats-it)

你可能觉得我在这里把事情想得有点过于简单了。垃圾回收器经常被比作魔法黑盒子。你的说法也对了一部分,实际情况要复杂得多。

例如,实际上,这个算法会与你的常规 Go 代码并行执行。遍历一个不断变化的图会带来挑战。我们还对这个算法进行了并行化,这一点稍后会再次提及。

但请相信我,这些细节大多与核心算法无关。核心算法实际上只是一个简单的图泛洪操作。

图洪水示例#graph-flood-example)

我们来看一个例子。请浏览下面的幻灯片,跟随步骤操作。

← 上一页下一页 →

img这里展示了一些全局变量和 Go 堆的结构图。让我们逐一分析。

img左侧是我们的根节点,即全局变量 x 和 y。它们将是我们图遍历的起点。由于它们被标记为蓝色,根据左下角的图例,它们目前位于我们的工作列表中。

img右侧是我们的待办事项列表。目前,列表中的所有内容都显示为灰色,因为我们还没有访问过其中的任何一项。

img每个矩形代表一个对象。每个对象都标有其类型。这个对象是类型为 T 的对象,其类型定义位于左上角。它包含一个指向子元素数组的指针和一个值。我们可以推测这是一种递归树状数据结构。

img除了类型为 T 的对象之外,您还会注意到我们还有包含 *T 的数组对象。这些数组对象由类型为 T 的对象的“children”字段指向。

img矩形内的每个小方格代表 8 字节的内存。带点的方格表示指针。如果方格内有箭头,则表示该指针不为空,指向其他对象。

img如果没有对应的箭头,那么它就是空指针。

img接下来,这些虚线矩形代表空余空间,我称之为空闲的“槽位”。我们可以在那里放置物体,但目前还没有物体。

img你还会注意到,对象是通过这些带标签的虚线圆角矩形分组在一起的。每个矩形代表一个页面,页面是一块连续的、固定大小且对齐的内存块。在 Go 语言中,页面大小为 8 KiB(与硬件虚拟内存的页面大小无关)。这些页面分别标记为 A、B、C 和 D,我将用这些标签来指代它们。

img在这个图中,每个对象都被分配到某个页面中。就像实际实现一样,这里的每个页面只包含特定大小的对象。这正是 Go 堆的组织方式。

img页面也是我们组织每个对象元数据的方式。这里可以看到七个方框,每个方框对应页面 A 中的七个对象槽位之一。

img每个方框代表一条信息:我们之前是否见过这个对象。实际上,运行时就是通过这种方式来管理对象是否已被访问过的,这一点稍后会很重要。

img细节很多,感谢阅读。这些内容之后都会派上用场。现在,我们先来看看我们的图表泛洪如何应用于这张图片。

img我们首先从工作列表中移除一个根节点。我们将其标记为红色,以表明它现在处于活动状态。

img沿着根指针,我们找到一个类型为 T 的对象,并将其添加到工作列表中。根据图例,我们将该对象绘制成蓝色,以表明它已在工作列表中。另请注意,我们在元数据中设置了与该对象对应的已查看位。

img下一个根节点也是如此。

img现在我们已经处理完了所有根节点,工作列表中还剩下两个对象。让我们从工作列表中移除一个对象。

img接下来我们要做的就是遍历对象的指针,以查找更多对象。顺便说一下,我们把遍历对象的指针称为“扫描”对象。

img我们找到了这个有效的数组对象……

img……并将其添加到我们的工作清单中。

img接下来,我们将递归地进行下去。

img我们遍历数组的指针。

img

img

img再找些东西……

img

img

img然后我们遍历数组对象引用的对象!

img请注意,我们仍然需要遍历所有指针,即使它们的值为零。我们事先并不知道它们的值是否会为零。

img这条树枝上还有一件东西……

img

img现在我们已经到达了另一个分支,从我们之前在某个根节点上找到的 A 页中的那个对象开始。

img您可能已经注意到,我们的工作列表遵循后进先出(LIFO)原则,这表明我们的工作列表是一个栈,因此我们的图泛洪算法近似于深度优先(DFO)。这是有意为之,反映了 Go 运行时中实际的图泛洪算法。

img我们继续……

img接下来我们找到另一个数组对象……

img然后走过去……

img

img

img

img

img工作清单上只剩最后一项了……

img我们来扫描一下……

img

img标记阶段完成!目前没有任何需要处理的对象,工作列表上也没有任何剩余任务。所有黑色绘制的对象都表示可达,所有灰色绘制的对象都表示不可达。让我们一次性清除所有不可达的对象。

img我们已将这些物品转换为空位,可以放置新物品。

问题#the-problem)

经过一番摸索,我认为我们已经掌握了 Go 垃圾回收器的实际工作原理。目前看来,这个过程运行良好,那么问题出在哪里呢?

事实证明,在某些程序中,执行这个特定算法会花费大量时间,而且几乎会给所有 Go 程序带来显著的开销。Go 程序将 20% 甚至更多的 CPU 时间用于垃圾回收的情况并不少见。

让我们来分析一下这些时间都花在了哪里。

垃圾收集成本#garbage-collection-costs)

从宏观层面来看,垃圾回收器的成本由两部分组成:一是运行频率,二是每次运行的工作量。将这两部分相乘,即可得到垃圾回收器的总成本。

GC总成本 = GC循环次数 × 每次GC循环的平均成本

多年来,我们一直在研究这个等式中的这两个术语。要了解垃圾回收器的运行频率,可以参考Michael 在 2022 年 GopherCon EU 大会上 关于内存限制的演讲。Go 垃圾回收器的指南也对此主题进行了大量阐述,如果您想深入了解,值得一看。

但现在我们只关注第二部分,即每个周期的成本。

多年来,我们不断研究 CPU 分析结果,试图提高性能,从中我们了解到 Go 的垃圾回收器有两大特点。

首先,垃圾收集器的成本约90%用于标记,只有约10%用于清扫。事实证明,清扫比标记更容易优化,而Go公司多年来一直拥有非常高效的清扫设备。

第二点是,在标记任务所花费的时间中,相当一部分(通常至少 35%)都浪费在了访问堆内存上。这本身就够糟糕的了,更糟糕的是,它完全阻碍了现代 CPU 真正高速运行的关键机制。

“微架构灾难” #a-microarchitectural-disaster)

在这个语境下,“gump up the works”是什么意思?现代CPU的具体构造相当复杂,所以我们用一个类比来解释。

想象一下,CPU 开车行驶在一条路上,这条路就是你的程序。CPU 想要加速到高速,为此它需要能够看到前方很远的路况,而且道路必须畅通无阻。但对于 CPU 来说,图洪水算法就像是在城市街道上开车。CPU 看不到拐角处的情况,也无法预测接下来会发生什么。为了前进,它不得不不断减速转弯、在红绿灯前停车、避让行人。你的引擎速度有多快几乎无关紧要,因为你根本没有机会加速。

让我们再来看一个例子,让它更具体一些。我在这里把堆垛和我们走过的路径叠加在一起了。每个从左到右的箭头代表我们完成的一项扫描工作,虚线箭头则表示我们在不同的扫描工作之间跳转。

img在我们的图洪水示例中,垃圾回收器在堆中执行的路径。

请注意,我们一直在内存中跳转,在每个地方都执行一些小操作。尤其值得注意的是,我们频繁地在页面之间跳转,以及在同一页面的不同部分之间跳转。

现代 CPU 会进行大量的缓存。访问主内存的速度可能比访问缓存中的内存慢 100 倍。CPU 缓存中存储的是最近访问过的内存,以及与最近访问过的内存位置相近的内存。但是,并不能保证两个相互指向的对象在内存中彼此靠近。图泛洪算法并没有考虑到这一点。

补充说明一下:如果只是延迟对主内存的读取操作,情况可能还不至于这么糟糕。CPU 异步发出内存请求,所以即使是慢速请求,如果 CPU 能提前看到足够多的信息,也能相互重叠。但在图洪水攻击中,每一项工作都很小、不可预测,而且高度依赖于前一项工作,因此 CPU 几乎每次读取内存都会被迫等待。

不幸的是,这个问题只会越来越严重。业内有句老话:“等两年,你的代码运行速度就会提升。”

但 Go 语言作为一种依赖标记清除算法的垃圾回收语言,却面临着相反的风险。“两年后,你的代码运行速度会变慢。” 现代 CPU 硬件的发展趋势正在给垃圾回收器的性能带来新的挑战:

非均匀内存访问。 首先,内存现在往往与部分 CPU 核心关联。其他CPU 核心访问这些内存的速度比以前慢。换句话说,主内存访问的成本取决于哪个 CPU 核心正在访问它。由于这种访问方式不均匀,我们称之为非均匀内存访问,简称 NUMA。

内存带宽降低。 每个 CPU 的可用内存带宽呈逐年下降趋势。这意味着,虽然我们拥有更多的 CPU 核心,但每个核心能够向主内存提交的请求相对较少,导致未缓存的请求需要等待更长时间。

CPU核心数量不断增加。 上文我们讨论的是顺序标记算法,但实际的垃圾回收器是并行执行该算法的。这种方式在CPU核心数量有限的情况下扩展性良好,但即使经过精心设计,待扫描对象的共享队列也会成为瓶颈。

现代硬件特性。 新硬件拥有诸如向量指令之类的强大功能,使我们能够一次性处理大量数据。虽然这有可能大幅提升速度,但如何将其应用于标记任务尚不明确,因为标记任务包含大量不规则且通常很小的运算。

绿茶#green-tea)

最后,我们来看看绿茶算法,这是我们对标记扫描算法的一种新方法。绿茶算法的核心思想非常简单:

操作页面,而不是对象。

听起来很简单,对吧?然而,为了弄清楚如何安排对象图遍历的顺序以及我们需要跟踪哪些内容才能使其在实践中有效运作,我们做了大量的工作。

更具体地说,这意味着:

  • 我们不扫描物体,而是扫描整页。
  • 我们不跟踪工作列表中的对象,而是跟踪整个页面。
  • 我们仍然需要在一天结束时标记对象,但我们会跟踪每个页面本地标记的对象,而不是跟踪整个堆中的标记对象。

绿茶示例#green-tea-example)

让我们再次查看我们的示例堆,看看这在实践中意味着什么,但这次运行的是 Green Tea 而不是直接的图洪水。

如上所示,请浏览带注释的幻灯片以跟随讲解。

← 上一页下一页 →

img这和之前的堆是一样的,但现在每个对象有两个元数据位,而不是一个。同样,每个位(或方框)对应于页面中的一个对象槽位。现在总共有十四个位,对应于页面 A 中的七个槽位。

img顶部的几位与之前相同,表示我们是否已经见过指向该对象的指针。我称这些位为“已见过”位。底部的几位是新的。这些“已扫描”位跟踪我们是否已经扫描过该对象。

img这条新的元数据是必要的,因为在 Green Tea 中,工作列表跟踪的是页面,而不是对象。我们仍然需要在某种程度上跟踪对象,而这正是这些元数据的作用。

img我们和以前一样,从根部开始移动物体。

img

img但这一次,我们不是在工作清单上放置一个对象,而是将一整页(在本例中是 A 页)放在工作清单上,用蓝色阴影标出。

img我们发现的这个对象也是蓝色的,这表明当我们从工作列表中移除此页面时,需要查看该对象。请注意,对象的蓝色色调直接反映了页面 A 中的元数据。其对应的“已查看”位已设置,但“已扫描”位未设置。

img我们沿着下一个根节点找到另一个对象,然后再次将整个页面(C 页)放入工作列表中,并设置对象的已查看位。

img我们已经完成了追溯根源的工作,所以我们转向工作清单,并从工作清单中取出 A 页。

img根据已看到和已扫描的部分,我们可以判断 A 页面上有一个需要扫描的对象。

img我们扫描该对象,并追踪其指针。因此,我们将页面 B 添加到工作列表中,因为页面 A 中的第一个对象指向页面 B 中的一个对象。

imgA页已经完成。接下来,我们从工作清单中移除C页。

img与 A 页类似,C 页上也只有一个物体需要扫描。

img我们在 B 页中找到了指向另一个对象的指针。B 页已经在工作列表中,所以我们不需要向工作列表中添加任何内容。我们只需要设置目标对象的已查看位即可。

img现在轮到B页了。我们在B页上积累了两个要扫描的对象,我们可以按内存顺序依次处理这两个对象!

img我们遍历第一个对象的指针……

img

img

img我们在 A 页中找到了一个指向对象的指针。A 页之前在工作列表中,但现在不在了,所以我们把它重新添加到工作列表中。与原始的标记清除算法不同,在原始算法中,每个给定对象在整个标记阶段最多只能添加到工作列表一次,而在 Green Tea 算法中,一个给定的页面在一个标记阶段可以多次出现在工作列表中。

img

img我们扫描页面上出现的第二个物体,紧接着扫描第一个物体。

img

img

img我们在A页还发现了一些其他物品……

img

img

img

imgB页扫描完毕,所以我们从工作列表中移除A页。

img这次我们只需要扫描三个物体,而不是四个,因为我们已经扫描过第一个物体了。我们可以通过查看“已识别”位和“已扫描”位之间的差异来确定要扫描哪些物体。

img我们将按顺序扫描这些物体。

img

img

img

img

img

img完成了!工作列表中已无页面,我们也没有任何正在查看的内容。请注意,由于所有可访问的对象都已被查看和扫描,因此元数据现在完全对齐。

img在遍历过程中,您可能也注意到工作列表的顺序与图洪水算法略有不同。图洪水算法采用的是后进先出(类似栈)的顺序,而这里我们对工作列表中的页面采用的是先进先出(类似队列)的顺序。

img这是有意为之。我们让已识别的对象在页面排队等待处理期间不断累积,这样我们就可以一次性处理尽可能多的对象。这就是我们能够在 A 页面上一次性访问如此多对象的原因。有时候,偷懒也是一种美德。

img最后,我们可以像以前一样,把那些无人问津的物品扫走。

驶入高速公路#getting-on-the-highway)

让我们回到开车的比喻。我们终于上高速公路了吗?

让我们回顾一下之前绘制的洪水图。

img原始图洪水在堆中经过的路径需要 7 次单独的扫描。

我们四处奔波,在不同的地方做着零零碎碎的工作。绿茶的发展道路看起来截然不同。

img绿茶的路径只需要扫描 4 次。

相比之下,绿茶的图案在A页和B页上的移动次数更少,但每次移动的距离更长。移动的距离越长效果越好,而且堆叠的图案越多,这种效果就越强烈。 这就是绿茶的魅力所在。

这也是我们驰骋高速公路的机会。

这一切都使得它与微架构更加契合。现在,我们可以更精确地扫描彼此靠近的对象,从而更有可能利用缓存并避免使用主内存。同样,每页的元数据也更有可能被缓存。跟踪页面而非对象意味着工作列表更小,而工作列表压力的降低意味着争用更少,CPU 停顿也更少。

说到高速公路,我们可以把我们比喻意义上的引擎开到以前从未开过的档位,因为现在我们可以使用矢量硬件了!

矢量加速度#vector-acceleration)

如果你对向量硬件只有粗浅的了解,可能会不明白我们在这里如何使用它。但除了常见的算术和三角运算之外,最新的向量硬件还支持两项对绿茶算法非常有用的功能:超宽寄存器和复杂的位运算。

大多数现代 x86 CPU 都支持 AVX-512 指令集,它拥有 512 位宽的向量寄存器。如此宽的寄存器足以在 CPU 上仅使用两个寄存器来存储整个页面的所有元数据,从而使 Green Tea 能够仅用几条直线指令就完成整个页面的扫描。向量硬件长期以来一直支持对整个向量寄存器进行基本的位运算,但从 AMD Zen 4 和 Intel Ice Lake 开始,它还支持一种新的位向量“瑞士军刀”指令,使得 Green Tea 扫描过程中的关键步骤能够在几个 CPU 周期内完成。这些改进共同作用,使我们能够大幅提升 Green Tea 的扫描循环速度。

对于图洪水来说,这根本不可能,因为我们需要在各种大小的对象之间来回扫描。有时只需要两条元数据,有时却需要一万条。矢量硬件根本无法满足这种可预测性和规律性要求。

如果你想了解一些细节,那就继续往下读吧!否则,你可以直接跳到评价部分

AVX-512 扫描内核#avx-512-scanning-kernel)

为了了解 AVX-512 GC 扫描的样子,请看下图。

在这里插入图片描述
!
用于扫描的 AVX-512 矢量内核。

这里面涉及的内容很多,我们可能光是解释它的运作原理就能写一整篇博客文章。现在,我们先从宏观层面来概括一下:

  1. 首先,我们获取页面的“已查看”和“已扫描”位。请记住,页面中的每个对象对应一位,并且页面中的所有对象大小相同。
  2. 接下来,我们比较这两个位集。它们的并集成为新的“扫描”位,而它们的差集则是“活动对象”位图,它告诉我们在本次页面扫描过程中(与之前的扫描相比)需要扫描哪些对象。
  3. 我们计算两个位图的差值并进行“扩展”,这样就不是每个对象占用一位,而是页面中的每个字(8 字节)占用一位。我们称之为“活动字”位图。例如,如果页面存储 6 个字(48 字节)的对象,则活动对象位图中的每位将被复制到活动字位图中的 6 位。如下所示:
0 0 1 1 ...

000000 000000 111111 111111 ...
  1. 接下来,我们获取页面的指针/标量位图。同样,这里的每一位都对应页面的一个字(8 字节),并告诉我们该字是否存储指针。这些数据由内存分配器管理。
  2. 现在,我们取指针/标量位图和活动字位图的交集。结果就是“活动指针位图”:该位图告诉我们尚未扫描的任何活动对象中包含的整个页面中每个指针的位置。
  3. 最后,我们可以遍历页面内存并收集所有指针。逻辑上,我们遍历活动指针位图中的每个置位,加载该字处的指针值,并将其写回缓冲区。该缓冲区稍后将用于标记已访问的对象并将页面添加到工作列表中。利用向量指令,我们只需几条指令即可一次处理 64 字节。

之所以速度如此之快,部分原因在于这VGF2P8AFFINEQB条指令,它是 x86 扩展“伽罗瓦域新指令”的一部分,以及我们前面提到的位操作“瑞士军刀”。它才是真正的核心,因为它使我们能够非常高效地执行扫描内核中的步骤 (3)。它执行按位仿射变换,将向量中的每个字节本身视为一个 8 位数学向量,并将其与一个 8x8 位矩阵相乘。所有这些操作都在伽罗瓦域 上进行GF(2),这意味着乘法运算是 AND 运算,加法运算是 XOR 运算。最终结果是,我们可以为每个对象大小定义几个 8x8 位矩阵,它们能够精确地执行我们需要的 1:n 位扩展。

完整的汇编代码请参见此文件。“展开器”针对每种大小类别使用不同的矩阵和不同的排列,因此它们位于一个由代码生成器 生成的单独文件中。除了展开函数之外,代码量并不多。由于我们可以对完全存储在寄存器中的数据执行上述大部分操作,因此大部分代码都得到了极大的简化。而且,希望不久之后这段汇编代码就能被 Go 代码取代

感谢奥斯汀·克莱门茨设计了这套流程。它简直太棒了,而且速度惊人!

评估#evaluation

以上就是它的工作原理。那么,它究竟有多大帮助呢?

效果可能相当显著。即使不考虑向量增强,我们的基准测试套件也显示垃圾回收的 CPU 成本降低了 10% 到 40%。例如,如果应用程序 10% 的时间都花在了垃圾回收器上,那么根据工作负载的具体情况,整体 CPU 消耗将降低 1% 到 4%。垃圾回收 CPU 时间降低 10% 大致是典型的改进幅度。(更多细节请参见GitHub 问题。)

我们在谷歌内部推广了绿茶,并且大规模推广后也看到了类似的效果。

我们仍在逐步推出向量增强功能,但基准测试和早期结果表明,这将额外减少 10% 的 GC CPU 使用率。

虽然大多数工作负载都能在一定程度上受益,但也有一些工作负载不会受益。

Green Tea 算法基于这样的假设:我们可以一次性在单页上累积足够多的对象进行扫描,从而抵消累积过程的成本。如果堆结构非常规则(对象大小相同,且在对象图中的深度也相近),那么这个假设显然成立。但是,有些工作负载通常要求我们每次只能扫描一个对象。这可能比图洪水更糟糕,因为我们可能在尝试累积对象到页面上的过程中,反而做了更多工作,最终却失败了。

Green Tea 算法针对仅包含单个待扫描对象的页面进行了特殊处理。这有助于减少回归错误,但并不能完全消除它们。

然而,要超越图洪流算法,所需的单页累积数据量远比你想象的要少。这项研究的一个意外发现是,每次仅扫描页面 2% 的数据就能取得比图洪流算法更好的性能。

可用性

Green Tea 已作为一项实验性功能包含在最新的 Go 1.25 版本中,可通过GOEXPERIMENTgreenteagc构建时设置环境变量来启用。但这并不包含前面提到的向量加速。

我们计划在 Go 1.26 中将其设为默认垃圾回收器,但您仍然可以GOEXPERIMENT=nogreenteagc在构建时选择禁用它。Go 1.26 还将为较新的 x86 硬件添加向量加速,并根据我们目前收集到的反馈进行一系列调整和改进。

如果可以,我们鼓励您尝试使用 Go 的最新版本!如果您更喜欢使用 Go 1.25,我们也同样欢迎您的反馈。请参阅这条 GitHub 评论,其中详细说明了我们希望看到的诊断信息(如果您可以分享这些信息),以及我们推荐的反馈渠道。

旅程

在结束这篇博文之前,让我们花点时间谈谈我们走到今天的历程,以及这项技术背后的人性因素。

绿茶的核心理念看似简单,就像某个人灵光一闪的灵感火花。

但事实并非如此。“绿茶”是许多人多年来共同努力和构思的成果。Go 团队的多位成员都参与了构思,包括 Michael Pratt、Cherry Mui、David Chase 和 Keith Randall。当时在英特尔工作的 Yves Vandriessche 的微架构见解也对设计探索起到了至关重要的作用。为了使这个看似简单的理念得以实现,我们尝试了许多方法,也处理了许多细节问题。

img时间线描绘了我们在达到今天这种状态之前,尝试过的一些类似想法。

这个想法的萌芽可以追溯到2018年。有趣的是,团队里的每个人都认为最初的想法是别人提出的。

绿茶这个名字是在2024年得来的。当时,奥斯汀在日本四处寻觅咖啡馆,喝了无数抹茶,并由此构思出了早期版本的原型!这个原型证明了绿茶的核心理念是可行的。从此,我们便开始了绿茶的研发之路。

在 2025 年,随着 Michael 将绿茶项目实施并投入生产,其理念进一步发展和变化。

这需要大量的协作探索,因为绿茶算法不仅仅是一个算法,而是一个完整的设计空间。我们认为,单凭我们中的任何一个人都无法独自驾驭它。仅仅有想法是不够的,你还需要弄清楚细节并加以验证。现在我们已经做到了,终于可以开始迭代了。

绿茶的未来一片光明。

请再次尝试设置一下GOEXPERIMENT=greenteagc,然后告诉我们效果如何!我们对这项工作感到非常兴奋,并期待听到您的反馈!

总结

Go 1.25版本中的Green Tea垃圾回收器通过优化内存访问模式和支持向量硬件加速,显著提升了垃圾回收的效率。通过以页面为单位进行标记和扫描,Green Tea减少了内存访问的跳跃,提升了缓存命中率,从而加速了程序的执行。尽管某些特定的工作负载可能不会获得显著的性能提升,但Green Tea在大多数应用中表现出色。随着Go 1.26版本的发布,Green Tea将成为默认的垃圾回收器。

本文详细介绍了Green Tea的工作原理和性能优势,如果你还没有尝试过,快去试试吧!并在Go社区提供你的反馈,帮助我们进一步优化这个强大的工具。

本文被猫头虎的Go生态洞察专栏收录,详情请点击Go生态洞察


参考资料

  1. Go 1.25官方文档:https://golang.org/doc/go1.25
  2. Go垃圾回收器指南:https://go.dev/doc/gc-guide
  3. GopherCon 2025视频:https://www.youtube.com/watch?v=07wduWyWx8M

下一篇预告

在下一篇文章中,我将为大家带来关于Go垃圾回收器的最新进展,特别是如何利用Green Tea垃圾回收器提升Go的内存管理效率。敬请期待!

学会Golang语言,畅玩云原生,走遍大小厂~💐


在这里插入图片描述

🐅🐾猫头虎建议Go程序员必备技术栈一览表📖:

☁️🐳 Go语言开发者必备技术栈☸️:
🐹 GoLang | 🌿 Git | 🐳 Docker | ☸️ Kubernetes | 🔧 CI/CD | ✅ Testing | 💾 SQL/NoSQL | 📡 gRPC | ☁️ Cloud | 📊 Prometheus | 📚 ELK Stack |AI


🪁🍁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批评指正!🐅🐾🍁🐥

学习 复习 Go生态

粉丝福利


👉 更多信息:有任何疑问或者需要进一步探讨的内容,欢迎点击文末名片获取更多信息。我是猫头虎,期待与您的交流! 🦉💬


联系我与版权声明 📩

  • 联系方式
    • 微信: Libin9iOak
    • 公众号: 猫头虎技术团队
    • 万粉变现经纪人微信: CSDNWF
  • 版权声明
    本文为原创文章,版权归作者所有。未经许可,禁止转载。更多内容请访问猫头虎的博客首页

点击✨⬇️下方名片⬇️✨,加入猫头虎AI编程共创社群。一起探索科技的未来,共同成长。🚀

在这里插入图片描述

在这里插入图片描述

Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐