golang 栈扩容

316 阅读4分钟

cloud.tencent.com/developer/a…

分段栈和连续栈

Go 1.3 版本前使用的栈结构是分段栈,随着goroutine 调用的函数层级的深入或者局部变量需要的越来越多时,运行时会调用 runtime.morestackruntime.newstack创建一个新的栈空间,这些栈空间是不连续的,但是当前 goroutine 的多个栈空间会以双向链表的形式串联起来,运行时会通过指针找到连续的栈片段:

  • 如果当前 goroutine 的栈几乎充满,那么任意的函数调用都会触发栈的扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split)。

为了解决这个问题,Go在1.2版本的时候不得不将栈的初始化内存从4KB增大到了8KB。后来把采用连续栈结构后,又把初始栈大小减小到了2KB。

连续栈

连续栈可以解决分段栈中存在的两个问题,其核心原理就是每当程序的栈空间不足时,初始化一片比旧栈大两倍的新栈并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。使用连续栈机制时,栈空间不足导致的扩容会经历以下几个步骤:

  1. 调用用runtime.newstack在内存空间中分配更大的栈内存空间;
  2. 使用runtime.copystack将旧栈中的所有内容复制到新的栈中;
  3. 将指向旧栈对应变量的指针重新指向新栈
  4. 调用runtime.stackfree销毁并回收旧栈的内存空间;

栈区的内存管理

前面说了每个goroutine都维护着自己的栈区,栈结构是连续栈,是一块连续的内存,在goroutine的类型定义的源码里我们可以找到标记着栈区边界的stack信息,stack里记录着栈区边界的高位内存地址和低位内存地址:

全局栈缓存

栈空间在运行时中包含两个重要的全局变量,分别是 runtime.stackpoolruntime.stackLarge,这两个变量分别表示全局的栈缓存和大栈缓存,前者可以分配小于 32KB 的内存,后者用来分配大于 32KB 的栈空间:

栈内容的申请也是跟前面文章里的一样,先去当前线程的对应尺寸的mcache里去申请,不够的时候mache会从全局的mcental里取内存

其实从调度器和内存分配的角度来看,如果运行时只使用全局变量来分配内存的话,势必会造成线程之间的锁竞争进而影响程序的执行效率,栈内存由于与线程关系比较密切,所以在每一个线程缓存 runtime.mcache中都加入了栈缓存减少锁竞争影响

// Per-thread (in Go, per-P) cache for small objects.

type mcache struct {
...
    stackcache [_NumStackOrders]stackfreelist
...
}

栈扩容

编译器会为函数调用插入运行时检查runtime.morestack,它会在几乎所有的函数调用之前检查当前goroutine 的栈内存是否充足,如果当前栈需要扩容,会调用runtime.newstack 创建新的栈:

旧栈的大小是通过我们上面说的保存在goroutine中的stack信息里记录的栈区内存边界计算出来的,然后用旧栈两倍的大小创建新栈,创建前会检查是新栈的大小是否超过了单个栈的内存上限。

栈缩容

goroutine运行的过程中,如果栈区的空间使用率不超过1/4,那么在垃圾回收的时候使用runtime.shrinkstack进行栈缩容,当然进行缩容前会执行一堆前置检查,都通过了才会进行缩容

如果要触发栈的缩容,新栈的大小会是原始栈的一半,不过如果新栈的大小低于程序的最低限制 2KB,那么缩容的过程就会停止。缩容也会调用扩容时使用的 runtime.copystack 函数开辟新的栈空间,将旧栈的数据拷贝到新栈以及调整原来指针的指向。

// Global pool of spans that have free stacks.
// Stacks are assigned an order according to size.
//     order = log_2(size/FixedStack)
// There is a free list for each order.
var stackpool [_NumStackOrders]struct {
   item stackpoolItem
   _    [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}
//go:notinheap
type stackpoolItem struct {
   mu   mutex
   span mSpanList
}
func stackinit() {
   if _StackCacheSize&_PageMask != 0 {
      throw("cache size must be a multiple of page size")
   }
   for i := range stackpool {
      stackpool[i].item.span.init()
      lockInit(&stackpool[i].item.mu, lockRankStackpool)
   }
   for i := range stackLarge.free {
      stackLarge.free[i].init()
      lockInit(&stackLarge.lock, lockRankStackLarge)
   }
}