深入了解 Go:垃圾回收机制

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

里克.哈德森

2018年7月12日

这是我在2018年6月18日国际内存管理研讨会上(ISMM)上发表的主题演讲的笔录。 在过去的25年中,ISMM一直是发布内存管理和垃圾回收文件的主要场所,并且也很荣幸一直被应邀作主题演讲。

摘要

Go语言的功能,目标和用例迫使我们重新考虑整个垃圾收集堆栈,并使我们进入了一个令人惊讶的地方。旅途令人振奋。 本场演讲描述了我们的旅程。 这是一个受开源和Google生产需求推动的旅程。其中包括在死胡同的峡谷中进行的徒步旅行,那里的数字引导我们回家。 本演讲将提供有关我们旅程的方式和原因,我们在2018年的位置以及Go为旅程的下一部分做准备的见解。

生态

理查德·哈德森(Rick)因其在内存管理方面的工作而闻名,其中包括发明了Train,Sapphire和Mississippi Delta算法以及GC堆栈映射,这些算法能够以静态类型的语言(例如Modula-3,Java,C#和Go)进行垃圾收集。 Rick目前是Google Go团队的成员,他致力于Go的垃圾回收和运行问题。

联系方式: rlh@golang.org

评论: 请参阅 关于golang-dev的讨论.

笔录

大家好,我是里克·哈德森(Rick Hudson)。

这是一个关于Go运行时间的讨论,尤其是关于垃圾收集器的讨论。我大约有45或50分钟的准备材料,之后我们会有时间讨论,并且我也会一直在附近,所以稍后可以随时提出问题。

在开始之前,我想先认识一些人。

演讲中的很多好东西都是由奥斯丁·克莱门茨(Austin Clements)完成的。Cambridge Go团队的其他成员Russ,Than,Cherry和David一直是一个充满吸引力,令人兴奋且有趣的团队。

我们还要感谢全球160万Go用户为我们解决了有趣的问题。没有他们,很多这些问题将永远不会浮出水面。

最后,我想感谢Renee French,感谢她多年来生产的所有这些不错的地鼠。您将在整个演讲中看到其中的几个。

在我们开始研究这些东西之前,我们确实必须展示GC对Go的看法。

首先,Go程序具有成千上万个堆栈。它们由Go调度程序管理,并且始终在GC安全点被抢占。 Go调度程序将Go例程多路复用到OS线程上,希望每个HW线程可以运行一个OS线程。我们通过复制堆栈并更新堆栈中的指针来管理堆栈及其大小。这是本地操作,因此可以很好地扩展

接下来重要的一点是,Go是类似于C的系统语言的传统中的一种面向值的语言,而不是大多数托管运行时传统语言中的面向引用的语言。例如,这显示了tar包中的类型在内存中的布局方式。所有字段都直接嵌入到Reader值中。这使程序员在需要时可以更好地控制内存布局。可以并置具有相关值的字段,这有助于提高缓存的位置。

以值为导向还有助于使用外部函数接口。我们拥有使用C和C ++的快速FFI。显然,Google有大量可用的功能,但是它们是用C ++编写的。 Go迫不及待地想在Go中重新实现所有这些功能,因此Go必须能够通过外部函数接口访问这些系统。

这一设计决定导致运行时必须执行一些更令人惊奇的事情。将Go与其他GC语言区分开来可能是最重要的事情。

当然,Go可以具有指针,实际上它们可以具有内部指针。这样的指针使整个值保持活动状态,并且它们很常见。

我们还有一种提前编译系统的方法,因此二进制文件包含整个运行时。

没有重新编译JIT。这有其优点和缺点。首先,程序执行的可重现性要容易得多,这使得编译器改进的步伐更快。

可悲的是我们并没有机会像使用JITed系统那样去进行反馈优化。

因此,这既有优点也有缺点。

Go有两个旋钮来控制GC。第一个是GCPercent。基本上,这是一个旋钮,用来调整你要使用的CPU数量和内存。默认值是100,这意味着一半的堆专用于活动内存,一半的堆专用于分配。您可以沿任一方向进行修改。

MaxHeap尚未发布,但正在内部使用和评估,它允许程序员设置最大堆大小。内存不足,OOM在Go上很难处理。内存使用量的暂时性峰值应通过增加CPU成本来解决,而不是中止。基本上,如果GC遇到内存压力,它将通知应用程序应该减轻负载。一旦一切恢复正常,GC就会通知应用程序它可以恢复到正常负载。 MaxHeap还为调度提供了更大的灵活性。运行时不必总是对可用的内存多少感到偏执,而是可以将堆的大小调整为MaxHeap。

这结束了我们对Go的那些对垃圾收集器很重要的部分的讨论。

现在,让我们谈谈Go运行时以及我们如何到达这里,如何到达自己所在的位置。

所以在2014年,如果Go不能以某种方式解决GC延迟问题,那么Go不会成功。这个道理显而易见。

其他新语言也面临同样的问题。 Rust之类的语言采用了不同的方式,但是我们将讨论Go所走的道路。

为什么延迟如此重要?

数学上完全无法容忍这一点。

99%ile隔离GC延迟服务级别重要的是整个会话期间的延迟或一天中多次使用应用程序的过程中的延迟。假设一个浏览了几个网页的会话在一个会话期间发出了100个服务器请求,或者它发出了20个请求,而您在一天中有5个会话打包。在这种情况下,只有37%的用户在整个会话中具有一致的低于10ms的体验。

正如我们所建议的那样,如果您希望这些用户中有99%的用户具有10ms以下的体验,那么数学计算表明您确实需要定位49s或99.99%ILE。

所以,在2014年,杰夫·迪恩(Jeff Dean)刚刚发表了他的论文——《规模的尾巴》,并进一步探讨了这一问题。由于它对Google的未来发展和试图在Google规模上继续扩大规模产生严重影响,因此在Google周围被广泛阅读。

我们称这个问题为9世纪的暴政。

那你如何对抗9世纪的暴政?

2014年,我们做了很多事情。

如果你想得到10个答案,请多问几个问题,然后取前10个,这些就是你在搜索页面上的答案。如果请求超过50%ILE,请重新发出或将请求转发到其他服务器。如果GC即将运行,则拒绝新请求或将请求转发到另一个服务器,直到GC完成。诸如此类。

所有这些变通办法都来自非常聪明的人,他们有非常实际的问题,但他们没有解决GC延迟的根本问题。但在Google规模上,我们又必须解决这些根本问题。为什么呢?

冗余无法扩展且成本很高。它花费了新的服务器场。

我们希望我们能解决这个问题,并把它看作是一个改善服务器生态系统的机会,在这个过程中拯救一些濒临灭绝的玉米地,让一些玉米粒在7月4日达到膝盖高度,并充分发挥其潜力。

这就是2014年SLO。是的,的确,我在打沙袋,在团队中是我的新手,这对我来说是一个新过程,我不想过分承诺。

此外,有关其他语言的GC延迟的演讲简直令人生畏。

最初的计划是做一个无读取障碍的并发复制GC。那是长远之计。读取障碍的开销存在很多不确定性,因此Go希望避免这些障碍。

但在2014年短期内,我们必须齐心协力。我们必须将所有运行时和编译器都转换为Go。他们当时是用C语言写的。没有更多的C语言,没有长尾的错误,因为C程序员不了解GC,但是对于如何复制字符串有一个很酷的想法。我们还需要一些快速且专注于延迟的东西,但性能影响必须小于编译器提供的加速。所以我们受到了限制。我们基本上有一年时间来对编译器性能进行改进,我们可以通过使GC并发来消耗掉这些性能。但仅此而已。我们不能放慢Go程序。这在2014年是站不住脚的。

所以我们退而求之,并不打算做复制的部分。

决定要做一个三色并发算法。在我职业生涯的早期,我和Eliot Moss做过日记证明,证明Dijkstra的算法可用于多个应用程序线程。我们还展示了我们可以解决STW(Stop-The-World的缩写,一种全局暂停现象)问题,并且我们有证据证明这是可以做到的。

我们还关注编译器的速度,即编译器生成的代码。如果我们在大多数时间关闭写屏障,编译器优化的影响将降至最低,并且编译器团队可以快速前进。Go也迫切地需要在2015年取得短期成功。

因此,让我们看一下我们所做的一些事情。

我们采用了大小分开的跨度。内部指针是一个问题。

垃圾收集器需要有效地找到对象的开始。如果知道跨度中对象的大小,则将其四舍五入为该大小,这将成为对象的开始。

当然,大小隔离的跨度还有其他一些优点。

低碎片化:使用C的经验,除了Google的TCMalloc和Hoard之外,我与Intel的可伸缩Malloc密切相关,这项工作使我们充满信心,碎片化对于不动的分配器不会成为问题。

内部结构:我们完全了解并拥有相关经验。我们了解了如何进行大小隔离的跨度,我们了解了如何进行低竞争或零竞争的分配路径。

速度:非复制与我们无关,诚然,分配可能会慢一些,但仍然是C的顺序。它可能不及凹凸指针那么快,但这没关系。

我们还遇到了这个外来功能接口问题。如果我们不移动对象,那么我们就不必处​​理如果您有一个移动的收集器(当您试图固定对象并将C和Go对象之间的间接层设置成水平)时可能遇到的bug的尾巴。一起工作。

下一个设计选择是放置对象元数据的位置。因为没有标题,所以我们需要有关对象的一些信息。标记位保留在侧面,用于标记和分配。每个单词都有2位与之关联,以告诉您它是标量还是该单词内的指针。它还对对象中是否有更多的指针进行了编码,因此我们可以早晚停止扫描对象。我们还有一个额外的位编码,可以用作额外的标记位或做其他调试工作。这对于使这些东西运行并查找错误非常有价值。

那么写障碍呢?仅在GC期间打开写屏障。在其他时候,编译后的代码将加载一个全局变量并对其进行查看。由于GC通常处于关闭状态,因此硬件会正确推测要在写屏障附近分支。当我们在GC中时,该变量是不同的,并且写屏障负责确保三色操作期间不会丢失任何可到达的对象。

该代码的另一部分是GC Pacer。这是奥斯丁所做的一些伟大的工作。它基本上基于确定何时最佳启动GC周期的反馈循环。如果系统处于稳定状态而不是处于相变状态,则标记将在内存耗尽时结束。

事实并非如此,因此Pacer还必须监视标记进度,并确保分配不会超出并发标记的范围。

如果需要,Pacer会减慢分配速度,同时加快标记速度。在较高的层次上,Pacer停止了Goroutine,该例程正在处理大量分配工作,并将其用于标记工作。工作量与Goroutine的分配成比例。这样可以加快垃圾收集器的速度,同时减慢转换器的速度。

完成所有这些步骤后,Pacer将从该GC周期以及先前的GC周期中学到的知识,并计划何时启动下一个GC。

它所做的不止于此,但这是基本的方法。

数学绝对令人着迷,请我阅读设计文档。如果您正在执行并发GC,那么您真的应该自己看一下这个数学,看看它是否与您的数学相同。如果您有任何建议,请告诉我们。

*执行1.5并发垃圾收集器的步调和[建议:单独的软堆和硬堆大小目标](github.com/golang/proposal/blob/ma... /design/14951-soft-heap-limit.md)

是的,所以我们取得了很多成功。一个年轻的疯狂疯子里克会拍下其中一些图表,并在我的肩膀上刺青它们,我为它们感到骄傲。

这是在Twitter上为生产服务器完成的一系列图形。当然,我们与该生产服务器无关。布赖恩·哈特菲尔德(Brian Hatfield)进行了这些测量,并奇怪地在推特上发布了有关它们的信息。

在Y轴上,我们有毫秒级的GC延迟。在X轴上,我们有时间。每个点都是该GC暂停世界暂停时间的时间。

在我们发布的第一个版本中,那是在2015年8月,我们看到了从大约300-400毫秒下降到30或40毫秒。这很好,数量级上很好。

我们将从此将Y轴从0到400毫秒彻底的更改为0到50毫秒。

这是6个月后。改进主要是由于系统的消除了世界停止期间(stop the world)我们正在做的所有O(heap)的事情。这是我们的第二个数量级改进,我们从40毫秒降低到4或5.

当时那里有一些错误,我们必须清理,我们在次要版本1.6.3中进行了次修正。这将延迟降低到不到10毫秒,这正是我们的服务级别目标(SLO)。

我们将要再次更改Y轴,这次是0到5毫秒。

所以到了这里,现在是2016年8月,第一次发布版本之后的一年。再次,我们不断取消这些O(堆大小)的停止世界(stop the world)进程。我们在这里谈论的是18Gbyte的堆。我们有大得多的堆,并且当我们取消这些O(堆大小)的停止世界暂停时,堆大小显然可以增加很多,,而不会影响延迟。因此,这对1.7版本有所帮助。

下一此发布版本是2017年3月。我们有了最后一次大的延迟下降,因为我们知道了如何避免GC周期结束时,世界堆栈扫描带来的停顿。那使我们跌入了毫秒一下的范围。同样,Y轴将更改为1.5毫秒,我们看到了我们第三次数量级的改进。

2017年8月版本几乎没有改善。我们知道是什么导致了其余的停顿。服务级别目标大大约为100-200微秒,我们将朝着这个方向努力。如果您看到几百微秒内的任何东西,那么我们真的很想与您交谈,弄清楚它是我们已知的原因导致的,还是它是否是我们未曾研究过的新事物。无论如何,似乎很少有人要求降低延迟。重要的是注意这些延迟级别可能由于多种非GC原因发生,并且俗话说:“您不必比熊快,您只需要比旁边的家伙快。”

18年2月1.10版中的内容没有太大变化,只是进行了一些清理和追查corner cases。

因此,新的一年和一个新的SLO这是我们的2018年SLO。

我们已将总CPU数降为GC周期内使用的CPU。

堆仍然是2倍。

我们现在的目标是每个GC中期停止世界暂停500微秒。也许这里有一些难度。

分配将继续与GC的助手成比例。

Pacer的状态已经好得多,因此我们希望在稳定状态下可以看到最少的GC助手。

我们对此感到非常满意。同样,这不是SLA(服务等级协议),而是SLO(服务等级目标),因此它是目标,而不是协议,因为我们无法控制操作系统之类的东西。

那是好的东西。让我们转移一下并开始谈论我们的失败。这些是我们的伤疤;它们有点像纹身,每个人都会有。无论如何它们都带有更好的估时,所以让我们谈一些这些故事。

我们的第一个尝试是做一些叫做面向请求的收集器或ROC的事情。假设可以在这里看到。

那么这是啥意思?

Goroutines是轻量级的线程,就想Gophers(地鼠)一样,因此这里有两个Goroutines。它们共享一些东西,例如中间的两个蓝色物体。它们具有自己的私有堆栈和私有对象。假设左边的家伙想共享绿色对象。

该goroutine将其放在共享区域中,以便其他Goroutine可以访问它。他们可以将其挂接到共享堆中的某个内容,或将其分配给全局变量,其他Goroutine可以看到它。

最终,左侧的Goroutine走向死亡之床,它即将死亡,令人难过。

如您所知,死后不能随身携带物品。您也无法拿走筹码。此时堆栈实际上是空的,并且对象不可访问,因此您可以简单地回收它们。

这里重要的是,所有操作都是本地操作,不需要任何全局同步。这从根本上不同于类似世代GC的方法,并且希望我们不必进行同步就可以进行扩展,这足以使我们获得胜利。

该系统正在发生的另一个问题是写入障碍一直存在。每当进行写操作时,我们都必须查看它是否正在将指向私有对象的指针写入公共对象。如果是这样,我们将必须将引用对象公开,然后遍历可访问对象,以确保它们也公开。那是一个非常昂贵的写障碍,可能导致许多高速缓存未命中。

就是说,哇,我们取得了一些相当不错的成功。

这是一个端到端的RPC基准测试。贴错标签的Y轴从0到5毫秒(越低越好),无论如何就是这样。 X轴基本上是镇流器或内核数据库的大小。

如您所见,如果您启用了ROC并且没有太多共享,那么实际上扩展的很好。如果您没有ROC,那还差那么一点。

但这还不够好,我们还必须确保ROC不会减慢系统的其他部分。那时,我们的编译器备受关注,我们不能放慢编译器的速度。不幸的是,编译器正是ROC不擅长的程序。我们看到30%,40%,50%甚至更多的速度下降,这是不可接受的。 Go为它的编译器有多快而感到骄傲,因此我们不能放慢编译器的速度,当然不会那么慢。

然后,我们去看了一些其他程序。这些是我们的性能基准。我们拥有200或300个基准的资料库,而这些基准是编译器人员认为对他们进行改进和改进很重要的基准。这些根本不是GC员工选择的。这个数字一直很糟糕,中华民国也不会成为赢家。

我们确实可以扩展,但是我们只有4到12个硬件线程系统,因此我们无法克服写障碍税。也许在将来我们拥有128个核心系统并且Go可以利用它们时,ROC的扩展属性可能是一个胜利。当发生这种情况时,我们可能会回来并重新审视此问题,但就目前而言,洛克石油是一个失败的主张。

那么我们接下来要做什么?让我们尝试分代GC。这是老歌,但好东西。 ROC无法正常工作,因此让我们回到具有更多经验的方面。

我们不会放弃等待时间,我们不会放弃我们不动的事实。因此,我们需要一个固定的世代GC。

那我们可以这样做吗?是的,但是对于分代GC,写屏障始终处于打开状态。当GC周期运行时,我们将使用与今天相同的写入屏障,但是当GC关闭时,我们将使用快速GC写入屏障来缓冲指针,然后在溢出时将缓冲区刷新到卡标记表中。

那么,这在不动的情况下如何工作?这是标记/分配图。基本上,您维护一个当前指针。分配时,您会寻找下一个零;找到零时,您会在该空间中分配一个对象。

然后,将当前指针更新为下一个0。

您继续操作,直到需要进行GC生成的时候。您会注意到,如果标记/分配向量中有一个,则该对象在最后一个GC上还活着,因此它已经成熟。如果它为零并且达到了它,那么您就知道它还很年轻。

那么,您如何推广。如果找到标记为1的东西指向标记为0的东西,则只需将零设置为1即可提升对象。

您必须经过传递,以确保所有可到达的对象都得到提升。

提升所有可到达的对象后,次要GC终止。

![](cdn.learnku.com/uploads/images/201...

最终,左侧的Goroutine走向死亡之床,它即将死亡,令人难过。

如您所知,死后不能随身携带物品。您也无法拿走筹码。此时堆栈实际上是空的,并且对象不可访问,因此您可以简单地回收它们。

这里重要的是,所有操作都是本地操作,不需要任何全局同步。这从根本上不同于类似世代GC的方法,并且希望我们不必进行同步就可以进行扩展,这足以使我们获得胜利。

该系统正在发生的另一个问题是写入障碍一直存在。每当进行写操作时,我们都必须查看它是否正在将指向私有对象的指针写入公共对象。如果是这样,我们将必须将引用对象公开,然后遍历可访问对象,以确保它们也公开。那是一个非常昂贵的写障碍,可能导致许多高速缓存未命中。

就是说,哇,我们取得了一些相当不错的成功。

这是一个端到端的RPC基准测试。贴错标签的Y轴从0到5毫秒(越低越好),无论如何就是这样。 X轴基本上是镇流器或内核数据库的大小。

如您所见,如果您启用了ROC并且没有太多共享,那么实际上扩展的很好。如果您没有ROC,那还差那么一点。

但这还不够好,我们还必须确保ROC不会减慢系统的其他部分。那时,我们的编译器备受关注,我们不能放慢编译器的速度。不幸的是,编译器正是ROC不擅长的程序。我们看到30%,40%,50%甚至更多的速度下降,这是不可接受的。 Go为它的编译器有多快而感到骄傲,因此我们不能放慢编译器的速度,当然不会那么慢。

然后,我们去看了一些其他程序。这些是我们的性能基准。我们拥有200或300个基准的资料库,而这些基准是编译器人员认为对他们进行改进和改进很重要的基准。这些根本不是GC员工选择的。这个数字一直很糟糕,中华民国也不会成为赢家。

我们确实可以扩展,但是我们只有4到12个硬件线程系统,因此我们无法克服写障碍税。也许在将来我们拥有128个核心系统并且Go可以利用它们时,ROC的扩展属性可能是一个胜利。当发生这种情况时,我们可能会回来并重新审视此问题,但就目前而言,洛克石油是一个失败的主张。

那么我们接下来要做什么?让我们尝试分代GC。这是老歌,但好东西。 ROC无法正常工作,因此让我们回到具有更多经验的方面。

我们不会放弃等待时间,我们不会放弃我们不动的事实。因此,我们需要一个固定的世代GC。

那我们可以这样做吗?是的,但是对于分代GC,写屏障始终处于打开状态。当GC周期运行时,我们将使用与今天相同的写入屏障,但是当GC关闭时,我们将使用快速GC写入屏障来缓冲指针,然后在溢出时将缓冲区刷新到卡标记表中。

那么,这在不动的情况下如何工作?这是标记/分配图。基本上,您维护一个当前指针。分配时,您会寻找下一个零;找到零时,您会在该空间中分配一个对象。

然后,将当前指针更新为下一个0。

您继续操作,直到需要进行GC生成的时候。您会注意到,如果标记/分配向量中有一个,则该对象在最后一个GC上还活着,因此它已经成熟。如果它为零并且达到了它,那么您就知道它还很年轻。

那么,您如何推广。如果找到标记为1的东西指向标记为0的东西,则只需将零设置为1即可提升对象。

您必须经过传递,以确保所有可到达的对象都得到提升。

提升所有可到达的对象后,次要GC终止。

最后,要完成分代GC循环,只需将当前指针设置回向量的开头,然后就可以继续。在该GC周期内未达到所有零,因此它们是免费的并且可以重用。众所周知,这被称为“粘性位”,是汉斯·勃姆(Hans Boehm)和他的同事们发明的。

那么性能如何?对于大堆来说还不错。这些是GC应该做得很好的基准。一切都很好。

然后,我们在性能基准上对其进行了测试,结果进展不顺利。那到底是怎么回事?

写障碍很快,但是根本不够快。此外,很难对其进行优化。例如,如果在分配对象的时间与下一个安全点之间存在初始化写操作,则会发生写障碍清除。但是我们必须转移到每条指令都具有GC安全点的系统上,因此实际上没有任何写障碍可以克服。

我们还进行了逃逸分析,并且越来越好。还记得我们在谈论的以价值为导向的东西吗?而不是传递指向函数的指针,我们将传递实际值。因为我们传递了一个值,所以转义分析只需要进行过程内转义分析,而无需进行过程间分析。

当然,在指向本地对象的指针转义的情况下,将为对象分配堆。

对于Go而言,并不是说世代假设不是正确的,只是年轻的对象在栈中年轻而活着而死。结果是,分代收集的效率远不如其他托管运行时语言所能找到的有效。

因此,抵制写障碍的这些力量开始聚集。如今,我们的编译器比2014年要好得多。转义分析收集了很多这些对象,并将它们粘贴在世代收集器本可以帮助的堆栈对象上。我们开始创建工具来帮助我们的用户找到逃脱的对象,如果它们很小,他们可以更改代码并帮助编译器在堆栈上分配。

用户对于采用面向价值的方法变得越来越聪明,并且指针的数量正在减少。数组和映射保存值,而不是结构的指针。万事皆安。

但是,这并不是Go语言中的编写障碍不断前进的主要原因。

让我们看一下这张图。这只是标记成本的分析图。每行代表一个可能具有标记成本的不同应用程序。假设您的标记成本是20%,这是很高的,但有可能。红线是10%,仍然很高。较低的线是5%,大约是这些天的写障碍成本。那么,如果将堆大小加倍,会发生什么呢?这就是右边的重点。由于GC循环的频率降低,标记阶段的累积成本大大降低。写屏障成本是恒定的,因此增加堆大小的成本将驱动标记成本低于写屏障成本。

这是写屏障的更常见成本,为4%,并且我们看到,即使这样做,我们也可以通过简单地增加堆大小来将标记屏障的成本降低到写屏障的成本以下。

世代GC的真正价值在于,当查看GC时间时,写屏障成本会被忽略,因为它们会涂在整个mutator上。这是世代GC的巨大优势,它可以大大减少完整GC周期的较长STW时间,但不一定会提高吞吐量。 Go并没有解决世界难题,因此必须更仔细地研究吞吐量问题,这就是我们所做的。

那是很多失败,而这种失败伴随着食物和午餐。我一直在抱怨“如果不是因为写障碍,Gee不会很棒。”

同时,奥斯汀(Austin)刚刚花了一个小时与Google的一些HW GC人员进行了交谈,他说我们应该与他们交谈,并尝试找出如何获得可能有帮助的HW GC支持。然后,我开始讲关于零填充高速缓存行,可重新启动的原子序列以及其他在大型硬件公司工作时无法实现的事情的战争故事。当然,我们将一些东西塞进了称为Itanium的芯片中,但我们无法将它们塞入当今更流行的芯片中。因此,故事的寓意只是使用我们拥有的硬件。

无论如何,我们都在谈论,疯狂的事情呢?

没有写障碍的卡标记怎么办?事实证明,奥斯汀拥有这些文件,并且他将所有出于某种原因他不告诉我的疯狂想法都写入了这些文件中。我认为这是某种治疗方法。我曾经和Eliot做过同样的事情。新想法很容易被粉碎,您需要保护它们并使它们变得更坚固,然后再将它们发布给世人。好吧,无论如何,他提出了这个主意。

这个想法是在每个卡中维护一个成熟指针的散列。如果将指针写入卡中,则哈希将更改,并且该卡将被视为已标记。这将用写屏障的成本换成散列的成本。

但更重要的是,它与硬件保持一致。

当今的现代体系结构具有AES(高级加密标准)指令。这些指令之一可以执行加密级哈希,而对于加密级哈希,如果我们也遵循标准的加密策略,则不必担心冲突。因此,散列不会花费太多,但我们必须加载要散列的内容。幸运的是,我们按顺序遍历了内存,因此我们获得了非常好的内存和缓存性能。如果您有一个DIMM并且击中了顺序地址,那将是一个胜利,因为它们比击中随机地址要快。硬件预取器将启动,这也将有所帮助。无论如何,我们有50年,60年的设计硬件来运行Fortran,C和SPECint基准测试。结果就是硬件可以快速运行这种东西也就不足为奇了。

我们进行了测量。很好这是大型堆的基准套件,应该不错。

然后,我们说性能基准是什么样子?不太好,几个异常值。但是现在,我们已经将写障碍从始终在变种器中移到了在GC周期中运行。现在,决定是否要进行世代GC的决定被推迟到GC周期开始之前。因为我们已经完成了卡片工作的本地化,所以我们在那里有了更多的控制权。现在,有了我们可以将其移交给Pacer的工具,它可以很好地动态截断那些落在右边且无法从世代GC中受益的程序。但这会赢吗?我们必须知道或至少考虑一下未来的硬件将是什么样。

未来的内存是什么?

让我们看卡你这个图。这是你们的经典的摩尔定律图。在Y轴上有一个对数的规模,显示出单个芯片上晶体管的数量。X轴是1971到2016之间的年份。我要指出的是,这几年有人预言摩尔定律已经失效了。

Dennard 规模增长在大约十年前结束了频率进步。新的生产过程需要更长的时间来适应。所以不是2年,现在是4年或更长时间。所以很明显,我们正在进入一个摩尔定律减速的时代。

让我们看看红圈中的芯片。这些是维持摩尔定律的最好的一代芯片。

它们是逻辑越来越简单并且重复多次的芯片。大量的相同的内核、多个内存控制器和缓存、GPU、TPU等等。

随着我们继续简化和增加复制,我们逐渐地以几根导线、一个晶体管和一个电容器结束。换句话说,是DRAM内存单元。

换句话说,我们认为加倍内存将比加倍核心更有价值。

原始图形 at www.kurzweilai.net/ask-ray-the-future-of-moores-law.

让我们开看看另一个关注于DRAM的图形。这个数字来自CMU最近的一篇博士论文。如果我们看这个,我们看到摩尔定律是蓝线。红线是容量,并且它看起来遵循摩尔定律。奇怪的是,我看到了一张可以追溯到1939年的图片,当时我们还在用drum内存,并且那个容量和摩尔定律一直在一起,所以这个图标已经持续了很长时间,肯定比这个房间里任何人或者的时间都要长。

如果我们将这个图表与CPU频率或各种“摩尔定律已死”图标进行比较,我们就会得出这样的结论:内存,或者至少是芯片容量,遵循摩尔定律的时间将比CPU更长。带宽(黄线),不仅与内存的频率有关,也与可从芯片上取下的管脚数量有关,因此它没有跟上,但也不差。

延迟(绿线),做得非常差,尽管我会注意到,顺序访问的延迟比随机访问的延迟更好。

(数据来自“了解和改善基于DRAM的内存系统的延迟,部分为了满足电气与计算机工程博士学位要求,Kevin K. Chang M.S., Electrical & Computer Engineering, Carnegie Mellon University B.S., Electrical & Computer Engineering, Carnegie Mellon University Carnegie Mellon University Pittsburgh, PA May, 2017")。查看Kevin K. Chang的论文。介绍中的原始图形不是我可以很容易地在其上画出摩尔定律的形式,所以我将X轴更改为更均匀。)

让我们去橡胶碰到路的地方(译者注:理论联系实际的地方)。这是实际的DRAM的定价,从2005年到2016年普遍下降。我选择了2005年,因为那是正是Dennard规模增长结束的时候,频率提高也是。

如果你看一下红色的圆圈,这基本上是我们减少Go的GC延迟的工作开始进行的时间,我们看到最初几年的价格表现不错。最近,情况不太好,因为过去两年供不应求,导致价格上涨。当然,晶体管并没有变得更大,在某些情况下,芯片容量已经增加,所以这是由市场力量推动的。RAMBUS和其他芯片制造商表示,向前推进,我们能看到下一个流程所见在2019到2020年的时间框架内。

我将避免投机内存行业中的全球市场力量,只会注意到定价是周期性的,并且从长期来看,供应有满足的趋势。

长期而言,我们相信内存定价将以比CPU定价快得多的速度下降。

(来源 hblok.net/blog/ 和 hblok.net/storage_data/storage_mem...)

让我们看这另一条线。如果我们在这条线上就太好了。这是SSD线路。它在保持低价方面做得很好。这些芯片的材料物理比DRAM复杂得多。逻辑更复杂,而不是每个单元一个晶体管,而是六个左右。

在DRAM和SSD之间有一条线,其中NVRAM,比如intel的3D XPoint和相变存储器(PCM)将存在。在接下来的十年中,这种类型内存的可用性增加可能会变得更加主流,,并且这只会强化添加内存是为我们的服务器增加价值的廉价方式的想法。

更重要的是,我们可以期待看到DRAM的其他竞争替代品。我不会假装知道五年或十年后哪一个更受欢迎,,但竞争将非常激烈,堆内存将更接近此处突出的蓝色固态硬盘行。

所有这些都加强了我们的决定,即避免始终打开的障碍,以利于增加内存。

那么所有这些对Go继续前进意味着什么?

我们打算使运行时更灵活和健壮,在查看一些来自我们的用户的corner cases时。我们希望收紧调度程序并获得更好的确定性和公平性,但我们不想牺牲任何性能。

我们也不打算增加GC API表层。我们已经有近十年,并且我们有两个旋钮,并且那感觉不错。没有对我们来说足够重要的应程序来添加新标志。

我们还将研究如何改进我们已经很好的逃逸分析,并为Go的面向价值的编程优化。不仅在编程中,而且在我们为用户提供的工具中。

在算法上,我们将专注于设计空间中可以最大限度减少障碍使用的部分,特别是那些一直处于打开状态的部分。

最后,也是最重要的,我们希望在接下来的5年中,甚至希望在未来的10年内,摩尔定律的趋势都更偏爱RAM而不是CPU。

就是这样,谢谢。

P.S. Go团队希望招聘工程师来帮助开发和维护Go运行时和编译器工具链。

感兴趣吗?看看我们的空缺职位

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/ismmkey...

译文地址:https://learnku.com/docs/go-blog/ismmkey...

上一篇 下一篇
Summer
贡献者:7
讨论数量: 0
发起讨论 只看当前版本


暂无话题~