189 8069 5689

go1.9语言gc go语言标准库中文版

Go GC:优先考虑低延迟和简单

Go 正在构建的 GC 不仅仅是面向于 2015 年,而是面向于 2025 年甚至更久之后:一个支持当局的软件开发并且可以同未来十年新的软件硬件一同扩展的 GC。

创新互联长期为上千多家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为河北企业提供专业的做网站、成都做网站河北网站改版等技术服务。拥有十余年丰富建站经验和众多成功案例,为您定制开发。

Go 1.5,是看到未来的第一瞥, 成功完成了我们一年前设定的 GC 延迟小于 10ms 的目标 。这篇文章综述了我们在 Go 1.5 的垃圾回收器上做了什么。

Go 的新 GC 是一个 并发的、三色的、标记清除的垃圾回收器(a concurrent, tri-color, mark-sweep collector)。我们相信它会非常适合于新时代硬件的性能和新时代软件的低延迟需求。

这些步骤都(?)发生在与应用程序并发阶段,(这个应用程序)被称为 mutator (翻译为“赋值器”),是它在垃圾回收进行时改变了变量的引用。

因为 mutator 必须保持没有黑色对象指向白色对象,以免 GC 跟丢它在堆上已经访问过的变量。

维持这种不变是 写屏障 (write barrier)的职责,它是 mutator 在堆中的指针被更新时运行的一个小函数(function)。

如果当前可达对象是白色,Go 的写屏障标记它为灰色,以确保 GC 最终能扫描到它的引用。

确认在什么时候所有的灰色对象被完全标记完了很微妙,而且如果我们想避免 block 应用程序(mutators)代价非常昂贵而且复杂。为了让事情简单, Go 1.5 做足了工作让它可以并行,然后短暂地 STW 去检查所有潜在的灰色对象。找到最终 STW 所需的时间与该 GC 所做的工作总量之间的最佳结合点是在 Go 1.6 主要交付的内容。

魔鬼在于细节。GC 过程的具体细节的低级讨论就不在这里讨论了(可参考原文有哪些问题)。

在更高级别上,解决 Go 性能问题的方法是增加了 GC 旋钮 (knobs),每一个性能问题有一个旋钮。程序员可以旋转这个旋钮来为他们的应用程序寻找合适的设置。不利的一面是,在十年之后你每年使用一个或两个新的旋钮,你终将遇到《GC 旋钮开关使用法》(GC Knobs Turner Employment Act),Go 不会走这条路。转而,Go 提供了一个旋钮,它叫 GOGC。

GOGC 控制了涉及到可达对象空间的堆的总大小。默认是 100,意味着在上一次收集之后,堆的大小比可达对象的空间大小大 100% 倍(也就是两倍大)。200 就意味着是 3 倍大。如果你想降低 GC 的总时间,那就增加 GOGC;如果你想用更多的 GC 时间来换取更少的内存,那就降低 GOGC。

更重要得,随着下一代硬件中 RAM 增倍(双倍),简单地设置 GOGC 增倍(双倍)将使用 GC 循环数减半。另一方面,既然 GOGC 是根据可达对象的空间大小来的,将可达对象增倍来让负载增倍不需要重调(retune)。应用自己扩展了。更进一步得,没有众多旋钮(knob)的困扰,团队可以专注于客户应用的反馈提高运行性能。

Go 1.5 指向了一个未来,在未来 STW 的暂停不再是使用安全语言的屏障,应用可以轻松得与硬件扩展,并且随着硬件变得越来越强大,GC 将不会成为更好、更具可扩展性的软件的障碍。

更多 1.5GC 的相关内容及我们怎么降低延迟问题可以参考: Go GC: Latency Problem Solved presentation 和 the slides 。

上面提到了这些术语和概念:

下面赋两张 PPT 中的图:

go gc gccgo gcc GNU 之间的关系

gc 与gccgo 都是go语言标准规范的不同实现,两者包含不同的侧重点:

使用成本上gccgo远比gc更高,基于如下原因:

总结:除非真要追求高性能,否则不建议去折腾gccgo

如果一定要折腾,建议思路:基于gcc docker 镜像,编写Dockerfile,安装golang,然后使用 go build -compiler=gccgo 。

相关资源:

Go 语言内存管理(三):逃逸分析

Go 语言较之 C 语言一个很大的优势就是自带 GC 功能,可 GC 并不是没有代价的。写 C 语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上。如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用 malloc 方法在堆上申请内存,如果程序不再需要这块内存了,再调用 free 方法释放掉。Go 语言不需要你主动调用 malloc 来分配堆空间,编译器会自动分析,找出需要 malloc 的变量,使用堆内存。编译器的这个分析过程就叫做逃逸分析。

所以你在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。

可逃逸分析并不是百分百准确的,它有缺陷。有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上。如果你了解 Go 语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效。

Go 语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识。

这里不对堆内存和栈内存的区别做太多阐述。简单来说就是, 栈分配廉价,堆分配昂贵。 栈空间会随着一个函数的结束自动释放,堆空间需要时间 GC 模块不断地跟踪扫描回收。如果对这两个概念有些迷糊,建议阅读下面 2 个文章:

这里举一个小例子,来对比下堆栈的差别:

stack 函数中的变量 i 在函数退出会自动释放;而 heap 函数返回的是对变量 i 的引用,也就是说 heap() 退出后,表示变量 i 还要能被访问,它会自动被分配到堆空间上。

他们编译出来的代码如下:

逻辑的复杂度不言而喻,从上面的汇编中可看到, heap() 函数调用了 runtime.newobject() 方法,它会调用 mallocgc 方法从 mcache 上申请内存,申请的内部逻辑前面文章已经讲述过。堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go 语言要消耗很多的计算资源对其进行标记回收(也就是 GC 成本)。

Go 编辑器会自动帮我们找出需要进行动态分配的变量,它是在编译时追踪一个变量的生命周期,如果能确认一个数据只在函数空间内访问,不会被外部使用,则使用栈空间,否则就要使用堆空间。

我们在 go build 编译代码时,可使用 -gcflags '-m' 参数来查看逃逸分析日志。

以上面的两个函数为例,编译的日志输出是:

日志中的 i escapes to heap 表示该变量数据逃逸到了堆上。

需要使用堆空间,所以逃逸,这没什么可争议的。但编译器有时会将 不需要 使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话:

多级间接赋值容易导致逃逸 。

这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go 语言中的引用类数据类型有 func , interface , slice , map , chan , *Type(指针) 。

记住公式 Data.Field = Value ,如果 Data , Field 都是引用类的数据类型,则会导致 Value 逃逸。这里的等号 = 不单单只赋值,也表示参数传递。

根据公式,我们假设一个变量 data 是以下几种类型,相应的可以得出结论:

下面给出一些实际的例子:

如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸。

上例中 te 的类型是 func(*int) ,属于引用类型,参数 *int 也是引用类型,则调用 te(j) 形成了为 te 的参数(成员) *int 赋值的现象,即 te.i = j 会导致逃逸。代码中其他几种调用都没有形成 多级间接赋值 情况。

同理,如果函数的参数类型是 slice , map 或 interface{} 都会导致参数逃逸。

匿名函数的调用也是一样的,它本质上也是一个函数变量。有兴趣的可以自己测试一下。

只要使用了 Interface 类型(不是 interafce{} ),那么赋值给它的变量一定会逃逸。因为 interfaceVariable.Method() 先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法。相当于 interfaceVariable.Method.this = realValue

向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 类型都会导致发送到 channel 中的数据逃逸。

这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配。

可变参数如 func(arg ...string) 实际与 func(arg []string) 是一样的,会增加一层访问路径。这也是 fmt.Sprintf 总是会使参数逃逸的原因。

例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2 级或更多级的访问赋值会 容易 导致数据逃逸。这里加上 容易 二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据。

下面代码中包含 2 种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么。

Benchmark 和 pprof 给出的结果:

熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化。

多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升。这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而 map , slice , interface{} 等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。

大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。


当前文章:go1.9语言gc go语言标准库中文版
文章源于:http://cdxtjz.cn/article/hpcegg.html

其他资讯