Golang运行时:P结构体中mspancache的缓冲区大小为何设为128,但allocMSpanLocked()仅使用一半

Golang运行时:P结构体中mspancache的缓冲区大小为何设为128,但allocMSpanLocked()仅使用一半

// allocMSpanLocked 分配一个 mspan 对象。
//
// 必须持有 h.lock。
//
// allocMSpanLocked 必须在系统栈上调用,因为其调用者持有堆锁。详情参见 mheap。
// 在系统栈上运行也确保了我们在此函数期间不会切换 P。详情参见 tryAllocMSpan。
//
//go:systemstack
func (h *mheap) allocMSpanLocked() *mspan {
	assertLockHeld(&h.lock)

	pp := getg().m.p.ptr()
	if pp == nil {
		// 我们没有 p,所以只需执行常规操作。
		return (*mspan)(h.spanalloc.alloc())
	}
	// 必要时重新填充缓存。
	if pp.mspancache.len == 0 {
		const refillCount = len(pp.mspancache.buf) / 2
		for i := 0; i < refillCount; i++ {
			pp.mspancache.buf[i] = (*mspan)(h.spanalloc.alloc())
		}
		pp.mspancache.len = refillCount
	}
	// 从缓存中取出最后一个条目。
	s := pp.mspancache.buf[pp.mspancache.len-1]
	pp.mspancache.len--
	return s
}

$ go version go version go1.16.4 linux/amd64

为什么 refillCount 的值只有 pp.mspancache.buf 的一半?


更多关于Golang运行时:P结构体中mspancache的缓冲区大小为何设为128,但allocMSpanLocked()仅使用一半的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

看起来这段代码片段来自这里

Ian Lance Taylor 在这个 golang-nuts 讨论串中提供了一个答案。

为其他读者提供参考:

因为 freeMSpanLocked 方法会尽可能将正在释放的 MSpan 添加到缓存中。如果缓存已满,那么 freeMSpanLocked 就必须将其释放到通用池中,以供未来的 allocMSpanLocked 调用使用。让缓存既不完全满也不完全空是更好的选择。将其设置为半满是一种折中方案。

Ian

更多关于Golang运行时:P结构体中mspancache的缓冲区大小为何设为128,但allocMSpanLocked()仅使用一半的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个关于Go内存分配器设计的深入问题。mspancache.buf大小为128但只使用一半的原因主要基于性能和内存效率的权衡。

核心原因:平衡分配速度和内存占用

mspancache是每个P(处理器)的本地缓存,用于快速分配mspan对象。缓存大小设为128提供了足够的缓冲区,但每次只填充一半(64个)是为了:

  1. 减少锁竞争时间allocMSpanLocked()在持有堆锁(h.lock)的情况下运行,填充缓存时需要从中央分配器获取多个mspan。只填充一半可以减少持有锁的时间。

  2. 避免内存浪费:如果某个P暂时不需要大量mspan,缓存一半大小可以减少未使用内存的占用。

  3. 保持缓存热度:64个mspan通常足够应对大多数分配场景,同时保持缓存经常刷新。

示例代码说明这种设计模式:

// 类似的设计模式在其他缓存中也有体现
type p struct {
    // 其他字段...
    mspancache struct {
        buf [128]*mspan  // 静态数组,固定大小
        len int          // 实际使用长度
    }
}

// 当缓存为空时,只填充一半
func refillCache(pp *p, h *mheap) {
    if pp.mspancache.len == 0 {
        // 关键设计:只填充缓存容量的一半
        const refillCount = len(pp.mspancache.buf) / 2
        
        // 批量分配,减少锁开销
        for i := 0; i < refillCount; i++ {
            pp.mspancache.buf[i] = h.centralAlloc()
        }
        pp.mspancache.len = refillCount
    }
}

// 分配时从缓存尾部取
func allocFromCache(pp *p) *mspan {
    if pp.mspancache.len == 0 {
        return nil
    }
    pp.mspancache.len--
    return pp.mspancache.buf[pp.mspancache.len]
}

性能考虑的具体体现:

// 假设缓存已满(128个)和半满(64个)的对比:
// 情况1:填充128个需要的时间
func fillFullCache() {
    start := time.Now()
    for i := 0; i < 128; i++ {
        allocFromCentral()  // 需要获取堆锁
    }
    lockDuration := time.Since(start)
    // 锁持有时间较长,影响其他goroutine
}

// 情况2:填充64个需要的时间  
func fillHalfCache() {
    start := time.Now()
    for i := 0; i < 64; i++ {
        allocFromCentral()  // 需要获取堆锁
    }
    lockDuration := time.Since(start)
    // 锁持有时间减半,减少竞争
}

这种设计是Go运行时在分配速度内存效率之间的典型权衡。128的总大小确保在突发分配时有足够缓冲,而每次只填充一半减少了锁竞争和内存浪费。

回到顶部