Golang: 切片扩容机制源码分析

前情提要

最近在复习Golang的切片知识,在网上看大佬们的博客讲解,自己在本地测试发现和博客讲的对不上。
自己查阅源码后,发现是Golang版本不同,每个版本对切片扩容机制的实现也不同。
因此保存一篇博客来记录Golang切片的扩容机制,以及如何阅读这部分源码。
尽管之后版本变动,也可以再通过源码的方式来进行查证。

注明:
本博客只是提供一种了解Golang切片扩容机制的方式,仅供参考。
因为不同版本下扩容机制的实现可能不同,小伙伴如果想要了解自己Go版本的切片扩容机制,也可以参考下本博客的方式。

源码分析

Golang源码版本:1.19.5,mac的arm架构。源码下载链接:https://go.dev/dl/
在这里插入图片描述
切片本身具有3个基本要素:指向底层数组元素的指针、长度len、容量cap。
切片在append过程中,如果len > cap,就会触发扩容机制。
切片扩容机制相关的源码都位于go/src/runtime包下,切片在触发扩容后会调用runtime.growslice函数。
growslice函数的扩容机制主要代码如下:

func growslice(et *_type, old slice, cap int) slice {
   
	...
	newcap := old.cap
	doublecap := newcap + newcap
	// 如果期望容量 > 当前容量的2倍,扩容到期望容量
	if cap > doublecap {
   
		newcap = cap
	} else {
    // 期望容量 <= 当前容量的2倍
		const threshold = 256
		// 如果当前容量 < 256,容量翻倍
		if old.cap < threshold {
   
			newcap = doublecap
		} else {
    // 如果当前容量 >= 256
			// 循环扩容,初始值为当前容量,每次扩容为上次的25%,外加256*3/4=192,直到新容量 >= 期望容量
			for 0 < newcap && newcap < cap {
   
				newcap += (newcap + 3*threshold) / 4
			}
			if newcap <= 0 {
   
				newcap = cap
			}
		}
	}
	...
}

这里的参数cap看作期望容量,可以理解为append后的元素个数,也就是扩容后的切片长度,期望能够有的容量。
上述代码只会确定切片的大致容量,还需要根据切片中的元素类型大小对齐内存,这部分代码也在growslice函数中。
growslice函数的扩容后内存对齐机制主要代码如下:

func growslice(et *_type, old slice, cap int) slice {
   
	...
	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	switch {
   
	case et.size == 1:
		...
	case et.size == goarch.PtrSize: // 元素类型大小为8字节
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) // 内存对齐,获取对齐后真正要申请的内存大小
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize) // 内存对齐后计算真正的扩容后容量
	case isPowerOfTwo(et.size):
		...
	default:
		...
	}
	...
}

为了方便,这里只考虑切片元素类型为int64的内存对齐机制。
内存对齐时会调用runtime.roundupsize函数对内存进行向上取整,获取对齐后要申请的内存大小,计算出真正扩容后的容量。
roundupsize函数的内存对齐机制代码如下:

func roundupsize(size uintptr) uintptr {
   
	if size < _MaxSmallSize {
    // size < 32768,即扩容后容量 < 4096,需要根据class_to_size数组来对内存向上取整
		if size <= smallSizeMax-8 {
    // size <= 1024 - 8,即扩容后容量 <= 127
			// size_to_class8数组用来获取class_to_size数组的索引
			// divRoundUp(size, smallSizeDiv),相当于计算ceil(size / 8)
			return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
		} else {
    // size > 1024 - 8,即扩容后容量 > 127
			// size_to_class128数组用来获取class_to_size数组的索引
			// divRoundUp(size-smallSizeMax, largeSizeDiv),相当于计算ceil((size - 1024) / 128)
			return uintptr(class_to_size[size_to_class128[divRoundUp
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值