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
看起来这段代码片段来自这里。
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个)是为了:
-
减少锁竞争时间:
allocMSpanLocked()在持有堆锁(h.lock)的情况下运行,填充缓存时需要从中央分配器获取多个mspan。只填充一半可以减少持有锁的时间。 -
避免内存浪费:如果某个P暂时不需要大量
mspan,缓存一半大小可以减少未使用内存的占用。 -
保持缓存热度: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的总大小确保在突发分配时有足够缓冲,而每次只填充一半减少了锁竞争和内存浪费。

