如何看待golang gc 回收时间新的GC 的Proposal

本文是总结Go语言的低延迟垃圾回收机制GC突出之处。Pusher是一个简单的托管API,通过WebSockets集成到网络和移动应用程序或任何其他互联网连接的设备上,实现快速,轻松,安全地将实时双向功能。每天,Pusher实时发送数十亿条消息:从源发送信息到目标只需要100ms。我们如何实现这一目标? 一个关键因素是Go的低延迟垃圾回收器。垃圾收集器是实时系统的缓慢的元凶,因为他们会暂停程序运行。所以在设计我们的新消息总线时,我们仔细选择了语言。 Go注重低延迟 ,但我们很谨慎:Go真正实现这一目标? 如果是,如何?在这篇博文中,我们将看看Go的垃圾收集器。 我们将看看它是如何工作的(三色算法),为什么它这么杰出的工作(实现这样短的GC暂停),最重要的是,它是否真的这样工作(对GC暂停进行基准测试,并与其他语言进行比较)。从Haskell到Go我们一直在构建的系统是一个将已发布消息存储内存的pub / sub消息总线。Go这个版本是我们在用Haskell完成后第一个实现的重写。在发现GHC的垃圾收集器底层延迟问题后,我们在5月停止了对Haskell版本的工作。GHC的暂停时间与工作集的大小(即内存中的对象数量)成正比。在我们的例子中,我们在内存中有很多对象,这导致了几百毫秒的暂停时间。 这是一个问题,任何GC当它完成收集时会阻塞程序。进入Go以后,它不像GHC的停止世界收集器,Go的收集器与程序同时运行,这使得避免更长的停顿成为可能。 我们被Go注重低延迟特性鼓励,并发现它很有前途,特别是当我们读到其每一个新版本都有关改善延迟时。并发垃圾收集器如何工作?Go的GC如何实现这种? 其核心是三色标记-清除(tricolor mark-and-sweep)算法 。下面是一个动画(见原文动画),显示了算法的工作原理。 注意它让GC与程序同时运行; 这意味着暂停时间成为调度问题。 调度程序可以配置为仅在短时间内运行GC集合,与程序交织。 这对我们的低延迟要求是个好消息!上面的动画详细显示了标记阶段。 GC仍然有两个停止世界阶段:对根对象的初始堆栈扫描,以及标记阶段的终止。 令人兴奋的是,这种终止阶段,最近被淘汰 。 我们将在后面讨论这个优化。 在实践中,我们发现对非常大的堆这些阶段的暂停时间为&1ms,。使用GC,也有可能在多个处理器上并行运行GC。延迟与吞吐量如果GC针对很大的堆产生很低的延迟,为什么要使用stop-the-world收集器?是不是Go的垃圾收集器比GHC的stop-the-world的收集器更好 ?不一定。 低延迟是要付出成本的。 最重要的成本降低吞吐量 。并发性需要额外的工作来同步和复制,这使得程序可以做有用的工作。GHC的垃圾收集器针对吞吐量进行了优化,但Go针对了延迟优化。 在Pusher,我们关心延迟,所以对于我们这是一个很好的方案。并发垃圾回收的第二个成本是不可预测的堆增长。程序可以在GC运行时分配任意数量的内存。这意味着GC必须在堆达到目标最大之前运行。 但是如果GC运行得太快,那么将执行更多的收集。 这个代价是非常棘手。在Pusher,这种不可预测性不是一个问题;我们的程序倾向于以可预测的恒定速率分配存储器。它在实践中如何执行?到目前为止,Go的GC看起来很适合我们的延迟要求。 但它在实践中如何执行?今年早些时候,当调查Haskell实现中的暂停时间时,我们为测量暂停创建了一个基准测试。基准程序重复地将消息推送到大小受限的缓冲区中。 旧消息不断地过期并变成垃圾。堆大小保持很大,这很重要,因为必须遍历堆才能检测哪些对象仍被引用。这就是为什么GC运行时间与它们之间的活对象/指针的数量成比例。这里是Go中的基准,其中缓冲区被建模为数组:
package mainimport (
"time")const (
windowSize = 200000
= 1000000)type (
message []byte
buffer [windowSize]message)var worst time.Durationfunc mkMessage(n int) message {
m := make(message, 1024)
for i := range m {
m[i] = byte(n)
return m}func pushMsg(b *buffer, highID int) {
start := time.Now()
m := mkMessage(highID)
(*b)[highID%windowSize] = m
elapsed := time.Since(start)
if elapsed > worst {
worst = elapsed
}}func main() {
var b buffer
for i := 0; i < msgC i++ {
pushMsg(&b, i)
fmt.Println("Worst push time: ", worst)} 结合其它人关于Ocaml Racket 和Java的基准测试,下面是在我系统上的测试结果,右边是最长暂停时间ms:基准
最长暂停 (ms)OCaml 4.03.0 (map based) (manual timing)
2.21Haskell/GHC 8.0.1 (map based) (rts timing)
67.00Haskell/GHC 8.0.1 (array based) (rts timing) [1] 58.60Racket 6.6 experimental incremental GC (map based)
144.21(tuned) (rts timing) Racket 6.6 experimental incremental GC (map based)
124.14 (untuned) (rts timing) Racket 6.6 (map based) (tuned) (rts timing) [2]
113.52Racket 6.6 (map based) (untuned) (rts timing)
136.76Go 1.7.3 (array based) (manual timing)
7.01Go 1.7.3 (map based) (manual timing)
37.67Go HEAD (map based) (manual timing)
7.81Java 1.8.0_102 (map based) (rts timing)
161.55Java 1.8.0_102 G1 GC (map based) (rts timing)
153.89很惊讶Java两个测试表现很差,而OCaml表现非常好,不到?3毫秒暂停时间,是由于为老一代使用增量的GC算法。 (我们不选择OCaml的主要原因是它支持多核并行性不好)。如你所见,Go执行顺利,暂停时间约为7ms。 这符合我们的要求。..结论这项GC调查的关键是针对更低的延迟或更高的吞吐量进行优化。他们也可能执行更好或更差,这取决于您的程序堆使用率。 (有很多的对象吗?他们有长或短的生命吗?)重要的是要了解底层的GC算法,以决定它是否适合您的用例。 在实践中测试GC实现也很重要。您的基准测试应该与您打算实现的程序具有相同的堆使用率。 这将在实践中检查GC实施的有效性。正如我们所看到的,Go的实现不是没有故障,但在我们的情况下,问题是可以接受的。 我想在更多的语言中看到相同的基准,如果你想贡献:)尽管有一些问题,Go的GC相比其他GCed语言执行得很好。Go团队一直在改进延迟,并继续这样做。 我们对Go的GC,理论和实践感到满意。
最佳分辨率
OpenSource
Code & 2002-20golang(1)
然后再到这里
golang的垃圾回收采用的是&标记-清理(Mark-and-Sweep)&算法
就是先标记出需要回收的内存对象快,然后在清理掉;
在这里不介绍标记和清理的具体策略,只介绍 GC过程是怎么调度的以及stw相关
这个算法,会导致 stw (stop the world)
的问题,中断用户逻辑
触发GC机制
<span style="color:#.&&&&在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的<span style="color:#倍,若是则触发(主GC线程为当前M)
<span style="color:#.&&&&监控线程发现上次GC的时间已经超过两分钟了,触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)
每当触发的时候,在主GC线程中就会走如下的GC流程:
<span style="color:#.&&&&stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止
<span style="color:#.&&&&标记:分配gc标记任务,唤醒 gcproc个
M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠
<span style="color:#.&&&&清理:有一个单独的goroutine去清理已经标记的内存对象快
<span style="color:#.&&&&start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)
对于上面的三个步骤,分别解释:
stop the world:
<span style="color:#.&&&&设置gcwaiting=1,这个在每一个G任务之前会检查一次这个状态,如是,则会将当前M
<span style="color:#.&&&&如果这个M里面正在运行一个长时间的G任务,咋办呢,难道会等待这个G任务自己切换吗?这样的话可要等<span style="color:#ms啊,不能等!坚决不能等!
所以会主动发出抢占标记(类&#20284;于上一篇),让当前G任务中断,再运行下一个G任务的时候,就会走到第<span style="color:#步
<span style="color:#.&&&&一直等待所有的M进入休眠,此时所有的业务逻辑代码都停止
<span style="color:#.&&&&&根据gcproc的个数,分配成gcproc任务段;唤醒gcproc-1
个M来执行(当前M也算一个)
<span style="color:#.&&&&对于一个M,唤醒前设置它的helpgc标记,唤醒之后这个M会立马判断这个标记,如是,则开始做分配给自己的标记任务,如果先做完了,就会从别的M里面找一些来做
<span style="color:#.&&&&等每一个M都做完,会再次进入休眠
<span style="color:#.&&&&通过设置参数,可以以一个单独goroutine
&运行,这个功能是在<span style="color:#.3版本之后增加的,这样的话就直接到下一步了,清理过程不是stw的
<span style="color:#.&&&&也可以串行的在主GC线程执行;这样的话则清理过程也是stw的,
start the world:
<span style="color:#.&&&&设置gcwaiting=0
<span style="color:#.&&&&唤醒P个M来继续做G任务(此时没有helpgc标记),业务逻辑代码开始
是基于<span style="color:#.4
版本的,GC过程在标记过程是(STW)的
在<span style="color:#.5
版本里面对GC做了很大的优化;采用三色标记,将标记过程细化成三段,只有前后的两段是stw的;极大地缩短了gc的stw时间
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:969次
排名:千里之外成为主流语言,Golang急需解决的几个问题。 - Golang中国
10:41 发布 25630 次点击
最近编程语言当中,golang无疑是风生水起,年度语言,服务器端语言,并发语言,皇冠可谓不少。golang开发的初衷是替换掉c/c++,作为系统级语言,加上在1.3版本中打算将编译系统从原来c语言开发的plan 9编译器,改为golang实现,可谓野心勃勃。golang最令人赞美的就是简单的语法,你可能花不了一天就能掌握golang的语法,关键字。golang的goroutine和channel给了大家一种简单的并发编程模型(在此指出的是channel是另一种选择,并不是用来替换掉Lock机制的并发编程,而是相互补助)。
在这里,给golang泼泼冷水,或者说,如果golang想成为主流语言,还需要解决哪些重要的问题。
一个无锁的sched调度算法。 golang容许你创建成千上万的goroutine,和原生系统级线程不同,goroutine的调度并不是由系统内核来完成,而是golang自己的sched调度系统来完成。golang的sched调度系统采用比较著名的work-steel算法,大家都知道该算法里最核心的一个数据结构就是一个任务队列,如何保证高并发下该队列的正确性是该算法的重点,比较熟悉java的同学应该知道,大师doug lea威廉叔叔的fork-join并发框架采用一个64位 volatile long字段来保证队列的高并发不加锁的实现(注意volatile在java中与c++里面的语义有区别),而golang却采用锁的方式来实现并发访问,这个无疑就掉了一个档次,好在在golang 1.3的计划里面已经明确将sched的无锁列为了重要的工作。
一个更轻量级的内存管理框架。大家都知道google有一款很牛的性能分析和加强的框架叫google perftools,该框架里面有一个multi-threaded malloc() 。golang内部的内存管理框架采用的正式该框架,所以如果是golang程序就没有必要再链接perftools来提高性能。该框架对于一个通用框架是非常不错的,然而golang的内存分配在一开始就定义好了内存大小,64位系统为128G内存,所以这种情况下的内存分配再简单不过了,而采用了一个极其复杂的malloc是否有必要?虽社区时有人反应,就连russ cox也表达了需要简化的想法,遗憾的是并没有加入golang 1.3的计划当中。
一个优化的垃圾回收算法。 golang目前的垃圾回收机制极其幼稚,当系统使用内存到达上次gc回收的多少比例的时候进行一次全内存的标记扫描回收,默认该比例为2倍,也就是说默认下,64位系统你最多能使用64g内存,当然这是理想情况,现实中,你估计也不会放心在这种gc算法下创建一个大内存系统。CMS在java中得到广泛应用,也是实践中得出最好的gc。golang如果想成为主流语言,必须在gc上和java一个级别,否则只能常常简单golang vs Python的文章。目前来看,提高垃圾回收精度(golang在不确定内存中保存的对象时,会默认4个字节一次的依次扫描内存,这个实在是太慢啦),扫描阶段不暂停系统比较切合实际,分代分区就要长远来看吧。
一个Heap Dump工具。如果你不知道golang程序实例内存是如何分布的,想要优化你的程序,就是盲人摸象。网上有一位美团的同学,在一个线上系统中,发现使用map会使gc变慢,他只能猜测是gc对map有特殊的处理导致,其实实际的原因是一个map在gc的时候会保留一把锁,而gc无法并发的回收该map,如果该map是一个主体结构,cms gc就变成了 单线程扫描gc了,好在该问题在golang 1.2已经修复。可喜的是golang 1.3就有可能发布heap dump工具。
采用copy stack替换现有的split stack。golang最值得夸耀的是轻量级的goroutine,goroutine的管理都有golang自己来实现,每一个goroutine都会默认在heap当中分配一个4k的内存块来作为该goroutine的stack。和java不同的是,golang的stack理论上可以无限大,这要得益于golang的split stack机制,当goroutine stack满时,golang会再分配一个块内存来补充,从而形成一个无限大的stack。然而成也萧何,败也萧何。当stack变小时,golang就会释放掉空闲的stack,如果一个长时间运行的程序,stack肯定会频繁的伸缩,这样照成的内存分配和释放相当频繁,从而影响性能。好在目前ross cox已经决定采用copy stack的方式来解决该问题,而copy stack会不会造成内存多余占用的问题呢,比如一个goroutine一次大stack,以后的stack都很小,让我们拭目以待吧。
采用copy stack替换现有的split stack。 golang最值得夸耀的是轻量级的goroutine,goroutine的管理都有golang自己来实现,每一个goroutine都会默认在heap当中分配一个4k的内存块来作为该goroutine的stack。和java不同的是,golang的stack理论上可以无限大,这要得益于golang的split stack机制,当goroutine stack满时,golang会再分配一个块内存来补充,从而形成一个无限大的stack。然而成也萧何,败也萧何。当stack变小时,golang就会释放掉空闲的stack,如果一个长时间运行的程序,stack肯定会频繁的伸缩,这样照成的内存分配和释放相当频繁,从而影响性能。好在目前ross cox已经决定采用copy stack的方式来解决该问题,而copy stack会不会造成内存多余占用的问题呢,比如一个goroutine一次大stack,以后的stack都很小,让我们拭目以待吧。
golang在语法上我想是无可挑剔了,上面是我认为golang急需解决的问题,如果能解决这些问题,特别是gc和调度,我想我会头也不回的投入golang的阵营当中。
xiaochouyu 于
10:47 修改
看来研究Go语言也挺多的,我觉得你说的这几个问题解决肯定是没有问题的,毕竟Go的发展时间比较短,而且GC这个问题也是很多人提到了。开发的思想是过早的优化是没有必要的,现在既然知道GC有性能问题,我觉得改善是必然的。
语法离无可挑剔还有不少距离,比如泛型的缺乏。泛型并非必不可少,但没有确实挺痛苦,高效可复用的算法和容器就不做指望了。
不需要c++那种变态级别的,只要一般程度的泛型就好了
不需要c++那种变态级别的,只要一般程度的泛型就好了
我喜欢go就是因为他没有泛型 :- )
UI框架也是必须
不记得这是谁说的了:
Go 里面最好的都不是创新的, 创新了的都不是最好的!
我认同, Go 语言基本没有什么创新.
用 Go 就是因为它足够简单, 能编译.
比起 C++ java , Go 简洁够用易用, 比起脚本语言, Go 可编译, 性能好, 写起来也慢不了多少.
最重要的是:对于我来说, 能力有限, 也写不出什么复杂的程序, 要是能写, 基本用什么语言都一样.
期待Go在这些方面的改善,尤其是性能,希望Go在补充了UI的同时可以在性能上更近一步优化,现在的版本确实已经明显变快了不少
语法上,没有三目运算符,写条件赋值代码真是很不爽,尤其是涉及到要赋值的对象需要初始化的时候。
写的很好,但是总觉得 golang 应该抛弃 gc,在讲究性能的开发目的下,依赖 gc 是一种坏的编程习惯。
后方可回复, 如果你还没有账号你可以
一个帐号。Sub-millisecond GC pauses」這邊看到的。
Golang想辦法將 GC 造成的影響降低:「
Proposal: Eliminate STW stack re-scanning」。
目標是解決最大的 GC pause 來源:
As of Go 1.7, the one remaining source of unbounded and potentially non-trivial stop-the-world (STW) time is stack re-scanning.
然後拿新的解法來戰,目前初步的測試看起來可以降到 50&s (== 0.05ms):
We propose to eliminate the need for stack re-scanning by switching to a hybrid write barrier that combines a Yuasa-style deletion write barrier [Yuasa '90] and a Dijkstra-style insertion write barrier [Dijkstra '78]. Preliminary experiments show that this can reduce worst-case STW time to under 50&s, and this approach may make it practical to eliminate STW mark termination altogether.
runtime: eliminate stack rescanning & Issue #17503 & golang/go」這邊可以看到進度,現在已經在 master branch 上了,看起來會在 1.9 的時候被放出來... 不過 worst case 的時間上修了 XDDD
The high level summary is that this reduces worst-case STW time to about 100 &s and typical 95%ile STW time to 50 &s (assuming, of course, that the OS doesn't get in the way and that the system isn't otherwise overloaded).
但看起來應該還是很大的效能改善,尤其是 CPU bound 的應用?
记录开发中遇到的Bug&持续更
最新教程周点击榜
2记录开发中遇到的Bug&持续更新...&
微信扫一扫推荐这篇日记的豆列
······}

我要回帖

更多关于 golang gc 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信