Go语言1.2今天发布了,其中有一项改变是默认的栈大小从之前的4096增加到了8K。记得早些时候就有人提到这个代码改动,还提醒大家注意,说每条网络连接开一个goroutine现在消耗内存会翻倍了。当时没有认真想,就觉得好像是那么回事。并且也没有深究Go为什么会做出这个改动。
直到今天看到Go 1.3的路线,说下个版本中将会做出的一个重大的改动就是不再使用分段栈的设计。这下我才觉得应该好好审视一下Go语言的分段栈设计了。
第一个问题就是:4k还是8k?
最初Go将一个goroutine的初始栈大小定为4k,正好一个操作系统内存页的大小,这是一个并没有经过深思熟虑的设计。这个值的大小本来是应该跟据大多数goroutine运行时消耗的栈大小来定的,但显然最初并没有做过这种统计。后面发现4k这个值不太够,于是增加到8k。
其实将这个值定为4k并没有直接收益,并不是4097就需要分配两次,而4k需要分配一次这种。
原因是其实存在两层抽象:下面一层是操作系统分页机制,不连续的物理内存以4k一页为单位被映射为了连续的虚拟内存;上面一层是分段栈机制,不连续的虚拟内存空间被以xx为单位映射为“连续”的栈空间提供给程序。正如下一层转化是通过缺页中断完成,上一层是通过栈越界检测加上Go运行时库完成的,具体就不展开讲了,反正我们可以把它当作另一种形式的“缺页中断”。
好了,现在清晰了。将goroutine初始栈大小定为4k,并不能减少我们上层的"缺页中断",如果goroutine普遍使用栈空间超过这个值,只会使得在上层的分配和释放变得更加频繁。另一方面,将这个值定为8k,也不一定增加下层的缺页中断,因为申请8k的内存,如果你并不使用这么多,其实不会引发缺页中断进而导致物理内存的分配。
那么前面那个人说的,一条连接开一个goroutine,内存使用会翻翻,其实也并没有这么夸张的。内存使用量确实可能会有一定的提高,但是是值得的。你想想,频繁的“不够,再拿,不够,再拿”,跟“多拿点吧,省得每次都要折腾”,哪种速度更快?
好吧,如果8k是一个合理的徝了,为什么Go准备放弃分段栈的设计呢?接下来看看,分段栈存在的问题。
分段栈在函数运行过程中需要更大栈空间时,会自动扩大,当函数不再需要,则会释放掉。一种很极端的情况就是,某函数的执行的栈大小一直在初始栈大小边界绯徊。那么就会不断地触发分段栈的分配回收操作,可以想象这是多么蛋疼的事情。
连续栈则可以避免这种问题。新的实现中,将给每个goroutine一个初始栈,当发生栈越界之后,就会重将分配一块更大的内存空间作为新的栈,将旧栈拷贝到新栈中。
这倒让我想起了以前写过的一个协程的玩具代码,也是使用栈拷贝。没测试过,自己也没底,只是当时看到云风大神也是这么做的,没想到现在Go也要这么做了。
有疑问加站长微信联系(非本文作者)