Go 协程堆栈设计进化之旅( 二 )


runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]0xc00002e800 - 0xc00002e000 = 2048runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]0xc000077000 - 0xc000076000 = 4096runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000]0xc000040000 - 0xc00003e000 = 8192runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000]0xc000082000 - 0xc00007e000 = 16384runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000]0xc00008a000 - 0xc000082000 = 32768我们可以看到在编译时 Goroutine 的栈空间初始大小为 2Kb, 在函数起始的地方增长到它所需要的大小 , 直到大小已经满足运行条件或者达到了系统限制 。
堆栈分配管理动态堆栈分配系统并不是唯一影响我们应用原因 。 不过 , 堆栈分配方式也可能会对应用产生很大的影响 。 通过两个完整的日志跟踪让我们试着理解它是如何管理堆栈的 。 让我们尝试从前两个堆栈增长的跟踪中了解 Go 是如何进行堆栈管理的:
runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800]copystack gp=0xc000000300 [0xc00002e000 0xc00002e6e0 0xc00002e800] -> [0xc000076000 0xc000076ee0 0xc000077000]/4096stackfree 0xc00002e000 2048stack grow doneruntime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000]copystack gp=0xc000000300 [0xc000076000 0xc000076890 0xc000077000] -> [0xc00003e000 0xc00003f890 0xc000040000]/8192stackfree 0xc000076000 4096stack grow done第一条指令显示了当前堆栈的地址 ,stack=[0xc00002e000, 0xc00002e800],并把他复制到新的堆栈里 , 并且是之前的二倍大小 ,copystack [0xc00002e000 [...] 0xc00002e800] -> [0xc000076000 [...] 0xc000077000], 4096 字节的长度和我们上面看到的一样 。 然后之前的堆栈将被释放: stackfree 0xc00002e000。 我们画了个图可以帮助理解上面的逻辑:
Go 协程堆栈设计进化之旅文章插图
Golang stack growth with contiguous stack
copystack 指令复制了整个堆栈 , 并把所有的地址都移向新的堆栈 。 我们可以通过一段简短的代码来很容易的发现这个现象:
func main() {var x [10]intprintln( --tt-darkmode-bgcolor: #131313;">打印出来的地址为
0xc00002e738[...]0xc000089f38地址 0xc00002e738 是被包含在我们之前看到的堆栈地址之中 stack=[0xc00002e000, 0xc00002e800], 同样的 0xc000089f38 这个地址也是包含在后一个堆栈之中 stack=[0xc000082000, 0xc00008a000], 这两个 stack 地址是我们上面通过 debug 模式追踪到的 。 这也证明了确实所有的值都已经从老的堆栈移到了新的堆栈里 。
另外 , 有趣的是 , 当垃圾回收被触发时 , 堆栈会缩小(译者注:一点也不 interesting) 。
在我们的例子中 , 在函数调用之后 , 堆栈中除了主函数外没有其他的有效函数调用 , 所以在垃圾回收启动的时候 , 系统会将堆栈进行缩减 。 为了证明这个问题 , 我们可以强制进行垃圾回收:
func main() {var x [10]intprintln( --tt-darkmode-bgcolor: #131313;">Debug 程序会展示出堆栈缩减的日志:
func cshrinking stack 32768->16384copystack gp=0xc000000300 [0xc000082000 0xc000089e60 0xc00008a000] -> [0xc00007e000 0xc000081e60 0xc000082000]/16384正如我们看到的这样 , 堆栈大小被缩减为原来的一半 , 并重用了之前的堆栈地址 stack=[0xc00007e000, 0xc000082000], 同样在 runtime/stack.go — shrinkstack() 中我们可以看到 , 缩减函数默认就是将当前堆栈大小除以 2:
oldsize := gp.stack.hi - gp.stack.lonewsize := oldsize / 2连续堆栈 VS 分段堆栈将堆栈复制到更大的堆栈空间中的策略称之为 连续堆栈(contiguous stack) , 与 分段堆栈(segmented stack)正好相反 。 Go 在 1.3 版本中迁移到了连续堆栈的策略 。 为了看看他们的不同 , 我们可以在 Go 1.2 版本中跑相同的例子看看 。 同样 , 我们需要修改 stackDebug 变量来展示 Debug 跟踪信息 。 为此 , 由于 Go 1.2 的 runtime 是用 C 语言写的 , 所以我们只能重新编译源代码. 。 这里是例子的运行结果:
func aruntime: newstack framesize=0x3e90 argsize=0x320 sp=0x7f8875953848 stack=[0x7f8875952000, 0x7f8875953fa0]-> new stack [0xc21001d000, 0xc210021950]func bfunc cruntime: oldstack gobuf={pc:0x400cff sp:0x7f8875953858 lr:0x0} cret=0x1 argsize=0x320当前的堆栈 stack=[0x7f8875952000, 0x7f8875953fa0] 大小是 8Kb (8192 字节 + 堆栈顶部的大小) , 同时新的堆栈创建大小为 18864 字节 ( 18768 字节 + 堆栈顶部的大小) 。 (译者注:这里比较难理解
0x7f8875953fa0 - 0x7f8875952000 并不到 8Kb , 应该是笔误 , 应该是 8096 字节)
内存大小分配的逻辑如下:
// allocate new segment.framesize += argsize;framesize += StackExtra;// room for more functions, Stktop.if(framesize