Go 博客
Go 垃圾回收器的旅程
这是我在 2018 年 6 月 18 日国际内存管理研讨会 (ISMM) 上发表的主旨演讲的文字记录。在过去的 25 年里,ISMM 一直是发布内存管理和垃圾回收论文的首要场所,我很荣幸被邀请发表主旨演讲。
摘要
Go 语言的功能、目标和用例迫使我们重新思考整个垃圾回收堆栈,并引导我们走向一个令人惊讶的地方。这段旅程令人振奋。本次演讲将描述我们的旅程。这是一段由开源和 Google 的生产需求驱动的旅程。其中包括进入死胡同峡谷的旁路远足,数字引导我们回家。本次演讲将深入了解我们旅程的来龙去脉、我们在 2018 年所处的位置以及 Go 为旅程的下一阶段所做的准备。
简介
Richard L. Hudson (Rick) 最出名的是他在内存管理方面的工作,包括发明 Train、Sapphire 和 Mississippi Delta 算法以及 GC 堆栈映射,这些算法使得在 Modula-3、Java、C# 和 Go 等静态类型语言中进行垃圾回收成为可能。Rick 目前是 Google Go 团队的成员,致力于 Go 的垃圾回收和运行时问题。
联系方式:[email protected]
评论:请参阅 golang-dev 上的讨论。
演讲记录
我是 Rick Hudson。
这是一场关于 Go 运行时,特别是垃圾回收器的演讲。我大约有 45 或 50 分钟的准备好的材料,之后我们将有时间进行讨论,我将留在这里,所以请随时在之后过来。
在我开始之前,我想感谢一些人。
演讲中很多好的内容都是由 Austin Clements 完成的。剑桥 Go 团队的其他成员,Russ、Than、Cherry 和 David,他们是一个令人愉快、激动人心且合作有趣的团队。
我们还要感谢全球 160 万 Go 用户,他们为我们提供了有趣的问题需要解决。如果没有他们,很多问题将永远不会被发现。
最后,我要感谢 Renee French 多年来制作的所有这些可爱的 Gopher。您将在整个演讲中看到其中几个。
在我们开始之前,我们真的需要展示 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 重新编译。这样做有优点和缺点。首先,程序执行的可重复性更容易实现,这使得改进编译器的工作变得更快。
不利的一面是我们没有机会像在 JIT 系统中那样进行反馈优化。
所以有好有坏。
Go 带有两个用于控制 GC 的旋钮。第一个是 GCPercent。基本上,这是一个旋钮,用于调整您想要使用的 CPU 量和内存量。默认值为 100,这意味着堆的一半用于活动内存,堆的一半用于分配。您可以朝任一方向修改此值。
MaxHeap 尚未发布,但已在内部使用和评估,它允许程序员设置最大堆大小。内存不足 (OOM) 对 Go 来说很困难;应该通过增加 CPU 成本来处理内存使用量的临时峰值,而不是通过中止来处理。基本上,如果 GC 发现内存压力,它会通知应用程序它应该减少负载。一旦情况恢复正常,GC 会通知应用程序它可以恢复到其常规负载。MaxHeap 还提供了更大的调度灵活性。运行时可以将堆的大小调整到 MaxHeap,而不是始终担心可用内存量。
这结束了我们关于对垃圾回收器很重要的 Go 部分的讨论。
现在让我们谈谈 Go 运行时以及我们是如何走到这一步的,以及我们是如何走到今天的。
所以是 2014 年。如果 Go 不以某种方式解决这个 GC 延迟问题,那么 Go 将不会成功。这一点很清楚。
其他新语言也面临着同样的问题。像 Rust 这样的语言采用了不同的方法,但我们将讨论 Go 采取的路径。
为什么延迟如此重要?
数学对此毫不留情。
99% 的隔离 GC 延迟服务水平目标 (SLO),例如 99% 的时间 GC 周期花费 < 10 毫秒,根本无法扩展。重要的是整个会话期间或在一天中多次使用应用程序的过程中出现的延迟。假设一个浏览多个网页的会话在会话期间最终发出 100 个服务器请求,或者它发出 20 个请求,并且您在一天中打包了 5 个会话。在这种情况下,只有 37% 的用户在整个会话中都能获得一致的低于 10 毫秒的体验。
如果您希望 99% 的用户获得低于 10 毫秒的体验,就像我们建议的那样,数学表明您实际上需要针对 4 个 9 或 99.99%。
所以是 2014 年,Jeff Dean 刚刚发表了他的名为“大规模尾部”的论文,它进一步探讨了这一点。由于它对 Google 的未来发展和尝试以 Google 规模进行扩展具有严重影响,因此它在 Google 内部被广泛阅读。
我们称这个问题为“9 的暴政”。
那么如何对抗“9 的暴政”呢?
2014 年做了很多事情。
如果您想要 10 个答案,请多问几个,然后取前 10 个,这些就是您放在搜索页面上的答案。如果请求超过 50%,则重新发出或将请求转发到另一台服务器。如果 GC 即将运行,则拒绝新请求或将请求转发到另一台服务器,直到 GC 完成。等等等等。
所有这些解决方法都来自非常聪明的人,他们遇到了非常现实的问题,但他们没有解决 GC 延迟的根本问题。在 Google 规模下,我们必须解决根本问题。为什么?
冗余将无法扩展,冗余成本很高。它需要新的服务器场。
我们希望能够解决这个问题,并将其视为改进服务器生态系统的机会,在此过程中节省一些濒危的玉米地,并给一些玉米粒机会在 7 月 4 日之前长到膝盖高,并充分发挥其潜力。
所以这是 2014 年的 SLO。是的,我确实在打马虎眼,我当时是团队的新成员,对我来说这是一个新的流程,我不想承诺过高。
此外,关于其他语言中 GC 延迟的演示简直令人恐惧。
最初的计划是做一个无读屏障并发复制 GC。那是长期计划。关于读屏障开销有很多不确定性,因此 Go 想要避免它们。
但 2014 年的短期目标是我们必须采取行动。我们必须将所有运行时和编译器转换为 Go。它们当时是用 C 编写的。不再使用 C,不再有由于 C 编码器不了解 GC 但对如何复制字符串有一个很酷的想法而导致的长尾错误。我们也需要一些快速的东西,并专注于延迟,但性能损失必须小于编译器提供的加速。所以我们受到限制。我们基本上有一年的编译器性能改进,我们可以通过使 GC 并发来消耗掉。但仅此而已。我们不能减慢 Go 程序的速度。这在 2014 年是不可接受的。
所以我们退了一步。我们不会做复制部分。
当时的决定是采用三色并发算法。在我职业生涯的早期,Eliot Moss 和我一起完成了期刊证明,证明了 Dijkstra 算法可以在多个应用程序线程中工作。我们还证明了可以消除 STW 问题,并且我们有证明可以做到这一点。
我们也担心编译器速度,也就是编译器生成的代码。如果我们大部分时间都保持写屏障关闭,编译器优化将受到最小的影响,编译器团队可以快速推进。Go 在 2015 年也迫切需要短期成功。
所以让我们看看我们做的一些事情。
我们采用了大小分段的 span。内部指针是一个问题。
垃圾收集器需要有效地找到对象的开头。如果它知道 span 中对象的尺寸,它只需向下舍入到该尺寸,这将是对象的开头。
当然,大小分段的 span 还有一些其他优势。
碎片率低:除了 Google 的 TCMalloc 和 Hoard 之外,我对 C 的经验,我还深度参与了 Intel 的 Scalable Malloc,这项工作让我们相信,对于非移动分配器来说,碎片化不会成为问题。
内部结构:我们完全理解并拥有相关的经验。我们了解如何进行大小分段的 span,我们了解如何进行低争用或零争用分配路径。
速度:非复制不让我们担心,分配可能速度较慢,但仍然是 C 级的速度。它可能不如 bump 指针快,但这没关系。
我们还有这个外部函数接口问题。如果我们不移动我们的对象,那么我们就不必处理在使用移动收集器时可能会遇到的大量 bug,因为您尝试固定对象并在 C 和您正在使用的 Go 对象之间放置间接级别。
下一个设计选择是将对象的元数据放在哪里。我们需要一些关于对象的信息,因为我们没有头部。标记位保存在旁边,用于标记和分配。每个字都有 2 位与之关联,以告诉您该字内部是标量还是指针。它还编码了对象中是否还有更多指针,以便我们可以比以前更早地停止扫描对象。我们还有一个额外的位编码,可以将其用作额外的标记位或执行其他调试操作。这对于运行这些东西和查找 bug 非常有价值。
那么写屏障呢?写屏障仅在 GC 期间开启。在其他时间,编译后的代码加载一个全局变量并查看它。由于 GC 通常是关闭的,因此硬件会正确地推测绕过写屏障的分支。当我们在 GC 内部时,该变量是不同的,并且写屏障负责确保在三色操作期间不会丢失任何可达对象。
这段代码的另一部分是 GC Pacer。这是 Austin 做的一些很棒的工作。它基本上基于一个反馈循环,该循环确定何时最好开始 GC 周期。如果系统处于稳定状态且不处于相变阶段,标记将在内存耗尽时结束。
情况可能并非如此,因此 Pacer 还必须监控标记进度并确保分配不会超过并发标记。
如果需要,Pacer 会减慢分配速度,同时加快标记速度。在高级别上,Pacer 会停止正在进行大量分配的 Goroutine,并将其用于执行标记。工作量与 Goroutine 的分配成正比。这加快了垃圾收集器的速度,同时减慢了 mutator 的速度。
完成所有这些操作后,Pacer 会从当前 GC 周期以及之前的周期中学习到的内容,并预测何时开始下一个 GC。
它做的不仅仅是这些,但那是基本方法。
数学绝对令人着迷,请 ping 我获取设计文档。如果您正在进行并发 GC,您真的应该自己看看这个数学,看看它是否与您的数学相同。如果您有任何建议,请告诉我们。
*Go 1.5 并发垃圾收集器配速 和 提案:分离软堆和硬堆大小目标
是的,所以我们取得了成功,很多成功。一个年轻、疯狂的 Rick 会把其中一些图表纹在我的肩膀上,我为它们感到非常自豪。
这是一系列为 Twitter 上的生产服务器完成的图表。我们当然与该生产服务器无关。Brian Hatfield 进行了这些测量,奇怪的是,他在推特上谈到了它们。
在 Y 轴上,我们以毫秒为单位显示 GC 延迟。在 X 轴上,我们有时间。每个点都是该 GC 期间的停止世界暂停时间。
在我们的第一个版本(2015 年 8 月)中,我们看到延迟从大约 300-400 毫秒下降到 30 或 40 毫秒。这很好,数量级上的提升。
我们将在这里彻底更改 Y 轴,从 0 到 400 毫秒降到 0 到 50 毫秒。
这是 6 个月后。改进主要归因于系统地消除了我们在停止世界时间内执行的所有 O(heap) 操作。这是我们数量级上的第二次改进,因为我们从 40 毫秒下降到 4 或 5 毫秒。
其中有一些 bug 我们需要清理,我们在 1.6.3 的小版本中进行了清理。这将延迟降低到远低于 10 毫秒,这是我们的 SLO。
我们即将再次更改 Y 轴,这次降到 0 到 5 毫秒。
所以我们到了这里,这是 2016 年 8 月,第一个版本发布一年后。我们再次不断消除这些 O(堆大小) 的停止世界进程。我们在这里讨论的是一个 18GB 的堆。我们有更大的堆,当我们消除这些 O(堆大小) 的停止世界暂停时,堆的大小显然可以大大增长而不会影响延迟。所以这对 1.7 来说是一个小小的帮助。
下一个版本是在 2017 年 3 月发布的。我们最后一次大幅降低延迟,这是由于弄清楚如何在 GC 周期的末尾避免停止世界堆栈扫描。这使我们进入了亚毫秒范围。Y 轴即将更改为 1.5 毫秒,我们看到了数量级上的第三次改进。
2017 年 8 月的版本几乎没有改进。我们知道是什么导致了剩余的暂停。这里的 SLO 低语数字约为 100-200 微秒,我们将朝着这个目标努力。如果您看到超过几百微秒的任何内容,那么我们真的想与您交谈,并确定它是否符合我们已知的内容,或者是否是我们尚未研究的新内容。无论如何,似乎很少需要更低的延迟。需要注意的是,这些延迟级别可能由于各种非 GC 原因而发生,正如俗话所说,“你不必比熊快,你只需要比你旁边的人快。”
2018 年 2 月的 1.10 版本没有实质性变化,只是一些清理和处理极端情况。
新的一年,新的 SLO,这是我们 2018 年的 SLO。
我们已将 GC 周期期间的总 CPU 使用率降低到 CPU 使用率。
堆仍然是 2 倍。
我们现在有一个目标,即每个 GC 周期的停止世界暂停时间为 500 微秒。也许这里有点保守。
分配将继续与 GC 辅助成正比。
Pacer 变得更好,所以我们希望在稳定状态下看到最少的 GC 辅助。
我们对此很满意。同样,这不是 SLA 而是 SLO,所以它是一个目标,而不是一个协议,因为我们无法控制操作系统等因素。
这些是好的方面。让我们转变一下,开始讨论我们的失败。这些是我们的伤疤;它们有点像纹身,每个人都会有。无论如何,它们伴随着更好的故事,所以让我们讲一些这些故事。
我们的第一次尝试是做一些叫做请求导向收集器或 ROC 的东西。假设可以在这里看到。
那这是什么意思呢?
Goroutines 是轻量级线程,看起来像 Gophers,所以这里我们有两个 Goroutines。它们共享一些东西,例如中间的两个蓝色对象。它们有自己的私有栈和自己的私有对象选择。假设左边的人想共享绿色对象。
该 goroutine 将其放入共享区域,以便另一个 Goroutine 可以访问它。它们可以将其挂钩到共享堆中的某个东西或将其分配给全局变量,另一个 Goroutine 可以看到它。
最后,左边的 Goroutine 走到生命的尽头,它即将死亡,很悲伤。
如您所知,您在死亡时无法带走您的对象。您也无法带走您的栈。此时栈实际上是空的,并且对象是不可达的,因此您可以简单地回收它们。
这里重要的是,所有操作都是本地的,不需要任何全局同步。这与分代 GC 等方法根本不同,并且希望从不进行同步而获得的扩展性足以让我们获得胜利。
该系统中发生的另一个问题是写屏障始终开启。每当发生写入时,我们都必须查看它是否正在将指向私有对象的指针写入公共对象。如果是这样,我们必须使被引用对象成为公共对象,然后进行可达对象的传递遍历,确保它们也成为公共对象。这是一个非常昂贵的写屏障,可能会导致许多缓存未命中。
也就是说,哇,我们取得了一些非常好的成功。
这是一个端到端的 RPC 基准测试。错误标记的 Y 轴从 0 到 5 毫秒(越低越好),无论如何,这就是它。
如您所见,如果您打开 ROC 并且没有太多共享,那么事情实际上扩展得非常好。如果您没有打开 ROC,那么它就不会那么好。
但这还不够,我们还必须确保 ROC 没有减慢系统的其他部分。那时,人们非常担心我们的编译器,我们不能减慢编译器的速度。不幸的是,编译器正是 ROC 表现不佳的程序。我们看到了 30%、40%、50% 甚至更多的速度下降,这是不可接受的。Go 为其编译器的速度感到自豪,所以我们不能减慢编译器的速度,当然不能减慢这么多。
然后我们去查看了一些其他的程序。这些是我们的性能基准。我们有一个包含 200 或 300 个基准的语料库,这些是编译器团队认为对他们来说很重要,需要进行改进的基准。这些基准完全不是由 GC 团队选择的。结果普遍很糟糕,ROC 不会成为赢家。
确实,我们的程序可以扩展,但我们只有 4 到 12 个硬件线程系统,因此无法克服写屏障带来的开销。也许将来当我们拥有 128 核系统,并且 Go 利用了这些系统时,ROC 的扩展特性可能会成为一个优势。当这种情况发生时,我们可能会重新审视这个问题,但就目前而言,ROC 并不是一个好的选择。
那么接下来我们要做什么呢?让我们尝试分代 GC。它是一个老方法,但仍然很好用。ROC 不起作用,所以让我们回到我们更有经验的东西上。
我们不会放弃我们的低延迟特性,也不会放弃非移动的特点。因此,我们需要一个非移动的分代 GC。
那么我们能做到吗?可以,但是使用分代 GC,写屏障始终处于开启状态。当 GC 循环运行时,我们使用与今天相同的写屏障,但在 GC 关闭时,我们使用一个快速的 GC 写屏障,该屏障缓冲指针,然后在溢出时将缓冲区刷新到卡片标记表中。
那么,这在非移动的情况下是如何工作的呢?这是标记/分配映射。基本上,你需要维护一个当前指针。当你分配内存时,你会寻找下一个零,当你找到这个零时,你就在该空间中分配一个对象。
然后你将当前指针更新到下一个 0。
你继续这个过程,直到某个时刻需要进行分代 GC。你会注意到,如果标记/分配向量中存在一个 1,则该对象在上一次 GC 时是存活的,因此它已经成熟。如果它是 0,并且你到达它,那么你知道它是一个年轻的对象。
那么如何进行提升呢?如果你发现一个标记为 1 的对象指向一个标记为 0 的对象,那么你只需将该 0 设置为 1 即可提升被引用对象。
你需要进行一次传递性遍历,以确保所有可达对象都被提升。
当所有可达对象都被提升后,次要 GC 终止。
最后,要完成你的分代 GC 循环,只需将当前指针设置回向量的开头,然后你就可以继续了。所有在该 GC 循环中未访问到的 0 都是空闲的,可以重复使用。正如你们许多人所知,这被称为“粘滞位”,是由 Hans Boehm 和他的同事发明的。
那么性能表现如何呢?对于大型堆来说还不错。这些是 GC 应该表现良好的基准。一切都很好。
然后我们在我们的性能基准上运行它,结果并不理想。到底发生了什么?
写屏障很快,但速度还不够快。此外,它很难进行优化。例如,如果在对象分配与下一个安全点之间存在初始化写入,则可以发生写屏障省略。但是我们不得不转向一个系统,在这个系统中,每条指令都存在一个 GC 安全点,因此在未来我们真的没有任何可以省略的写屏障。
我们还有逃逸分析,它越来越好了。还记得我们讨论过的面向值的特性吗?我们不再将指向函数的指针传递给函数,而是传递实际的值。因为我们传递的是值,所以逃逸分析只需要进行过程内逃逸分析,而不需要进行过程间分析。
当然,在指向局部对象的指针发生逃逸的情况下,该对象将被分配到堆上。
并不是说分代假设对 Go 不适用,而是年轻对象在栈上生存和死亡的时间都很短。结果是,分代收集的效率远低于你在其他托管运行时语言中发现的效率。
因此,这些反对写屏障的力量开始聚集。如今,我们的编译器比 2014 年要好得多。逃逸分析正在捕获许多这些对象并将它们放到栈上——这些对象本来是分代收集器可以帮助处理的。我们开始创建工具来帮助我们的用户找到发生逃逸的对象,如果逃逸是次要的,他们可以更改代码并帮助编译器在栈上分配内存。
用户越来越聪明地拥抱面向值的方法,指针的数量正在减少。数组和映射保存值,而不是指向结构体的指针。一切都很好。
但这并不是写屏障在 Go 中未来面临艰难挑战的主要原因。
让我们看看这张图表。它只是一个标记成本的分析图表。每条线代表一个可能具有标记成本的不同应用程序。假设你的标记成本是 20%,这相当高,但这是有可能的。红线是 10%,仍然很高。较低的线是 5%,大约是写屏障如今的成本。那么,如果将堆大小增加一倍会发生什么?这就是右侧的点。标记阶段的累积成本大幅下降,因为 GC 循环频率降低了。写屏障成本是恒定的,因此增加堆大小的成本将导致标记成本低于写屏障成本。
这是一个更常见的写屏障成本,即 4%,我们可以看到,即使在这种情况下,我们也可以通过简单地增加堆大小,将标记屏障的成本降低到写屏障成本以下。
分代 GC 的真正价值在于,当查看 GC 时间时,写屏障成本会被忽略,因为它们被分散到 mutator 中。这是分代 GC 的一大优势,它极大地减少了完整 GC 循环的长时间 STW 时间,但它并不一定会提高吞吐量。Go 没有这种停止世界的问题,因此它不得不更仔细地观察吞吐量问题,这就是我们所做的。
有很多失败,而失败也带来了食物和午餐。我正在像往常一样抱怨:“如果这不是写屏障就好了。”
与此同时,Austin 刚刚花了一个小时与 Google 的一些硬件 GC 团队成员交谈,他说我们应该和他们谈谈,并尝试弄清楚如何获得可能会有帮助的硬件 GC 支持。然后我开始讲述关于零填充缓存行、可恢复原子序列和其他一些在我为一家大型硬件公司工作时行不通的事情的故事。当然,我们确实将一些东西放入了名为 Itanium 的芯片中,但我们无法将它们放入当今更流行的芯片中。所以故事的寓意是,简单地使用我们现有的硬件。
无论如何,这让我们开始思考,有没有什么疯狂的想法?
有没有办法在没有写屏障的情况下进行卡片标记?事实证明,Austin 有这些文件,他将所有疯狂的想法都写到这些文件中,出于某种原因,他没有告诉我。我认为这是一种治疗方法。我以前也和 Eliot 做过同样的事情。新想法很容易被粉碎,在将其发布到世界之前,需要保护它们并使它们更强大。无论如何,他提出了这个想法。
这个想法是,在每个卡片中维护一个成熟指针的哈希值。如果将指针写入卡片,则哈希值将发生变化,并且该卡片将被视为已标记。这将用哈希的成本换取写屏障的成本。
但更重要的是,它与硬件对齐。
当今的现代架构拥有 AES(高级加密标准)指令。其中一条指令可以进行加密级别的哈希,使用加密级别的哈希,如果我们也遵循标准的加密策略,则无需担心冲突。因此,哈希不会花费我们太多成本,但我们必须加载要哈希的内容。幸运的是,我们正在顺序遍历内存,因此可以获得非常好的内存和缓存性能。如果你有一个 DIMM 并命中顺序地址,那么它会比命中随机地址更快。硬件预取器将启动,这也有帮助。无论如何,我们已经设计了 50 年、60 年的硬件来运行 Fortran、运行 C 以及运行 SPECint 基准测试。毫不奇怪,结果是能够快速运行此类任务的硬件。
我们进行了测量。这相当不错。这是大型堆的基准套件,应该表现良好。
然后我们问,对于性能基准来说,结果如何?不太好,有一些异常值。但现在我们已将写屏障从始终在 mutator 中开启更改为在 GC 循环中运行的一部分。现在,关于是否要进行分代 GC 的决策被延迟到 GC 循环开始时。我们在这里拥有更多控制权,因为我们已经将卡片工作本地化了。现在我们有了这些工具,可以将其交给 Pacer,它可以很好地动态地切断落在右侧且无法从分代 GC 中受益的程序。但这在未来会成为赢家吗?我们必须知道,或者至少考虑未来硬件将是什么样子。
未来的内存是什么样的?
让我们看看这张图表。这是你经典的摩尔定律图表。Y 轴是对数刻度,显示单个芯片中的晶体管数量。X 轴是 1971 年到 2016 年之间的年份。我需要指出的是,这些是有人在某个地方预测摩尔定律失效的年份。
Dennard 缩放大约十年前就结束了频率改进。新工艺的开发时间越来越长。因此,它们不再是 2 年,而是 4 年或更长时间。因此,很明显,我们正在进入摩尔定律放缓的时代。
让我们只看看红色圆圈中的芯片。这些是能够最好地维持摩尔定律的芯片。
它们是逻辑越来越简单并被复制多次的芯片。许多相同的内核、多个内存控制器和缓存、GPU、TPU 等等。
随着我们继续简化和增加复制,我们最终会得到几根线、一个晶体管和一个电容器。换句话说,就是一个 DRAM 内存单元。
换句话说,我们认为增加内存容量比增加内核数量更有价值。
原始图表 在 www.kurzweilai.net/ask-ray-the-future-of-moores-law。
让我们来看另一个专注于DRAM的图表。这些数据来自卡内基梅隆大学最近的一篇博士论文。如果我们观察这个图表,可以看到摩尔定律用蓝线表示。红线表示容量,它似乎遵循摩尔定律。奇怪的是,我看到一个图表可以追溯到1939年,当时我们使用的是磁鼓存储器,容量和摩尔定律也在一起稳步增长,所以这个图表已经存在很长时间了,肯定比在座的各位的年龄都要长。
如果我们将此图表与CPU频率或各种“摩尔定律已死”的图表进行比较,我们会得出结论:内存,或者至少是芯片容量,将比CPU更长时间地遵循摩尔定律。带宽(黄线)不仅与内存频率有关,还与芯片上引脚的数量有关,因此它没有跟上摩尔定律的步伐,但表现也不算差。
延迟(绿线)表现非常糟糕,不过我需要指出的是,顺序访问的延迟要优于随机访问的延迟。
(数据来自“理解和改进基于DRAM的内存系统的延迟,作为卡内基梅隆大学电气和计算机工程博士学位的部分完成要求提交。凯文·K·张硕士,电气与计算机工程,卡内基梅隆大学学士,电气与计算机工程,卡内基梅隆大学卡内基梅隆大学匹兹堡,宾夕法尼亚州2017年5月”。参见凯文·K·张的论文。引言中的原始图表并非易于绘制摩尔定律线的形式,因此我将X轴更改为更均匀的形式。)
让我们看看实际情况。这是实际的DRAM价格,从2005年到2016年总体呈下降趋势。我选择2005年是因为大约在那个时候Dennard缩放结束,频率改进也随之结束。
如果观察红圈,它基本上代表了我们减少Go的GC延迟的工作开展时间,我们可以看到,在最初的几年里,价格表现良好。最近,情况不太乐观,因为需求超过供应导致过去两年价格上涨。当然,晶体管并没有变得更大,在某些情况下芯片容量有所增加,因此这是由市场力量驱动的。Rambus和其他芯片制造商表示,展望未来,我们将在2019-2020年期间看到下一个制程工艺的缩减。
我将避免对内存行业的全球市场力量进行推测,仅需指出价格是周期性的,从长远来看,供应有满足需求的趋势。
从长远来看,我们相信内存价格下降的速度将比CPU价格下降的速度快得多。
(来源https://hblok.net/blog/和https://hblok.net/storage_data/storage_memory_prices_2005-2017-12.png)
让我们看看这条线。哇,如果我们在这条线上就好了。这是SSD线。它在保持价格低廉方面做得更好。这些芯片的材料物理学比DRAM复杂得多。逻辑更复杂,而不是每个单元一个晶体管,而是有六个或更多。
展望未来,在DRAM和SSD之间,将存在诸如英特尔的3D XPoint和相变内存(PCM)之类的NVRAM。在未来十年中,此类内存的可用性可能会变得更加主流,这只会加强以下观点:增加内存是为我们的服务器增加价值的廉价方法。
更重要的是,我们可以期待看到其他与DRAM竞争的替代方案。我不会假装知道五年或十年后哪一个会受到青睐,但竞争将非常激烈,堆内存将更接近此处突出显示的蓝色SSD线。
所有这些都强化了我们避免始终开启的屏障而选择增加内存的决定。
那么,所有这些对Go的未来意味着什么呢?
当我们查看来自用户的极端情况时,我们打算使运行时更加灵活和健壮。希望能够收紧调度程序,获得更好的确定性和公平性,但我们不想牺牲任何性能。
我们也不打算增加GC API的表面积。我们已经有了将近十年的时间,并且有两个旋钮,这感觉刚刚好。没有哪个应用程序对我们来说重要到需要添加新的标志。
我们还将研究如何改进我们已经非常出色的逃逸分析,并针对Go的面向值的编程进行优化。不仅在编程中,还在我们为用户提供的工具中。
在算法上,我们将专注于设计空间中最大程度减少屏障使用量的部分,特别是那些始终开启的屏障。
最后,也是最重要的是,我们希望利用摩尔定律有利于RAM而非CPU的趋势,至少在未来5年内,希望在未来十年内。
就是这样。谢谢。
附注:Go团队正在寻找工程师来帮助开发和维护Go运行时和编译器工具链。
有兴趣吗?看看我们的空缺职位。
下一篇文章:使用Go Cloud进行可移植的云编程
上一篇文章:更新Go行为准则
博客索引