Golang深入学习3-切片
本篇理解切片的底层实现和扩容方式。
1. 实现
切片的定义位于 src/runtime/slice.go
,如下
|
|
所以可见切片和字符串很相似,实质都是一个指针,只不过除了长度 len 还有一个容量字段 cap。一个简单的图解如下
图中的 x 和 y 都是从数组 [5]int{2,3,5,7,11} 上获取的切片,也就是指向该数组的不同位置。
上篇介绍字符串的时候提到字符串虽然底层是指针,但不允许等于 nil,它的空值是空字符串 ""
。但切片是可以等于 nil 的,只要其底层指针等于 nil,一般情况是切片声明而未初始化的时候出现该情况,这个时候因为没有指向任何内存区域,切片的长度和容量信息都是无效的,不过还是可以获取到。
注:因为切片等于 nil 一般意味着没有初始化,也就没有使用的价值,所以很少将切片直接和 nil 作比较,使用更多的还是判断切片的长度是否为0(len(s) == 0)
|
|
切片一旦初始化,底层指针就指向了一个确定的内存区域,但指向的内存区域大小可以为0,也就是切片中没有任何元素,此时切片的长度也是 0,但和未初始化时得到的长度绝不是一个含义。
|
|
2. 扩容
切片的长度是当前所包含的元素个数,容量是可容纳的最大元素个数。这里的含义是,初始化时指定的容量就代表在内存已经预分配了与容量相等的空间,其后访问、添加、删除切片的元素都和数组相似,只操作指针,不会造成内存重新分配。
|
|
但是,如果追加的元素数量超过了容量,那么会导致内存的重新分配。
|
|
内存的重新分配就是切片的扩容,其逻辑是,为切片分配一块更大的内存,然后将旧切片的元素复制到新切片中。
一个很有意思的情况如下,将切片 s 赋值给一个新的切片 l,然后对原切片 s 进行扩容和修改,不会影响到切片 l
|
|
最后一个值得注意的问题是切片每次扩容会扩大多少,这个逻辑位于 src/runtime/slice.go
文件中的 growslice 函数中,其中 old.len 是旧长度,old.cap 是旧容量,newcap 是新容量,cap 是需要的容量,
|
|
简单描述就是:
- 如果需要的容量超过原切片容量的两倍,直接使用需要的容量作为新容量;
- 如果原切片的长度小于 1024,新切片的容量翻倍;
- 如果原切片的长度大于1024,则每次增加25%,直到新容量超过所需要的容量;
第二条的翻倍倒是可以确认,但第一条和第三条经验证却不太符合
|
|
这是因为扩容的那一段核心源码后面还有一段新容量的处理过程,如下
|
|
其中 et 是切片中的元素类型,sys.PtrSize 是一个指针的大小,64位系统中为8,主要调用的处理函数 roundupsize 来自 src/runtime/msize.go
文件,如下
|
|
这里面涉及到了一些常量和两个函数,内容如下
|
|
由此可以得到处理逻辑:对前面的过程得到的新容量 cap,处理后调用 roundupsize 处理,如果容量小于 32768,那么依赖 class_to_size、size_to_class8 和 size_to_class128 三个数组中的两个去调整,得到的结果再稍微处理一下作为新容量。
以前面出错的两个例子计算,首先是需要的容量超过原切片容量两倍的,预处理得到的新容量为 5,然后因为 64 位系统中 int 类型为 8 个字节,调用 roundupsize 的方式是 capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
,也就是乘以8再传给 roundupsize 处理,这时 roundupsize 得到的参数是 40,40 < 32768,40 < 1024-8,divRoundUp(size, smallSizeDiv)
计算得到 5,获取 size_to_class8[5] = 4,再获取 class_to_size[4] = 48,最后返回,经过 newcap = int(capmem / sys.PtrSize)
处理得到新容量 48/8 = 6
|
|
第二个例子中,预处理得到的新容量是 1500,int 型8个字节,将 1500*8 = 12000 传入 roundupsize,12000 < 32768,但是 12000 > 1024-8,经 divRoundUp(size-smallSizeMax, largeSizeDiv)
计算得到 86,获取 size_to_class128[86] = 55,再获取 class_to_size[55] = 10880,返回后经 newcap = int(capmem / sys.PtrSize)
处理得到新容量 12288/8 = 1536
|
|
当然,这个处理过程非常复杂,平常使用一般不会自己去做这么复杂的计算,我们仅仅需要大致估计扩容后的大小就可以了,所以可以简单的使用前面的三条规则做估计,这里重复一下
- 如果需要的容量超过原切片容量的两倍,直接使用需要的容量作为新容量;
- 如果原切片的长度小于 1024,新切片的容量翻倍;
- 如果原切片的长度大于1024,则每次增加25%,直到新容量超过所需要的容量;