阅读 540

Golang切片扩容新论断

前言

对于slice的扩容规则,一直以来都流传一个说法:

  • 如果原slice的容量不足1024,那么该slice的容量会变为原来的2倍。

  • 如果原slice的容量大于等于1024,该slice会以1.25倍的不断扩容,直到容量满足要求。

以上两条规则相信大家在很多地方都看到过,说法起源已经无从得知。甚至在很多人给出的面试经验中,他们也是这样回答面试者的。

遗憾的是,这是错的。

案例

如果有下面这样一段代码,请问会输出什么内容?

nums := make([]int, 17)
nums = append(nums, 1)
fmt.Printf("cap: %d, len: %d\n", cap(nums), len(nums))
复制代码

相信有很大一部分人会说,很简单嘛,cap34len18

事实上,真正的输出如下:

cap: 36, len: 18
复制代码

等等,怎么和我们认知的规则很不一样。别急,继续往下看。

append扩容机制

通过查看slice源码,可以发现以下一段代码:

go/src/runtime/slice.go

func growslice(et *_type, old slice, cap int) slice {
	// ...... 省略一些判断
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
    // ......
}
复制代码

代码中的cap表示slice最少需要的容量,比如nums := make([]int, 16),如果想追加一个元素,容量最少得是17,此时cap就是17

以上代码规则如下:

  • cap大于原容量的两倍时,新的容量变为cap

  • cap小于原容量的两倍,且原容量小于1024时,新容量翻倍;

  • cap小于原容量的两倍,且原容量大于等于1024时,以1.25倍扩容,直到满足要求。

直到现在还没什么问题,基本和我们以往的认知差不多。但是,如果继续往下看,就能发现问题了。

go/src/runtime/slice.go

func growslice(et *_type, old slice, cap int) slice {
	switch {
	case et.size == 1:
    	// ......
		capmem = roundupsize(uintptr(newcap))
        // ......
	case et.size == sys.PtrSize:
    	// ......
		capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        // ......
	case isPowerOfTwo(et.size):
        // ......
		capmem = roundupsize(uintptr(newcap) << shift)
        // ......
	default:
    	// ......
		capmem = roundupsize(capmem)
        // ......
	}
}
复制代码

et.size可以理解为一个slice元素所占字节数,重点关注roundupsize,这个函数的作用是用于内存对齐,具体对齐规则也很好看懂,查看以下源代码即可:

go/src/runtime/msize.go:

func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= smallSizeMax-8 {
			return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
		} else {
			return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
		}
	}
	if size+_PageSize < size {
		return size
	}
	return alignUp(size, _PageSize)
}
复制代码

size_to_class8size_to_class是用来获取spanClass的。

class_to_size是用来获取span划分的object大小。

至于上述变量的具体含义和数据内容,可自行查阅源码获取。

经过内存对齐之后,获得的容量才是我们最终得到的结果。

结论

slice的对齐规则并没有我们认为的那么简单,不仅仅是翻倍,也不仅仅是1.25倍扩容,还需要考虑所需容量和原容量的大小,以及内存对齐产生的影响。

具体对齐规则可以手动查阅相关源代码,如果有不正确或者要补充的,欢迎交流。

文章分类
后端
文章标签