三.slice切片的底层
对应问题
切片底层(跟谁学,好未来,伴鱼,知乎,七牛+3,京东,哔哩哔哩,腾讯+1,小米,字节,Aibee,网易)
slice,len,cap,共享,扩容(腾讯)
append底层 (七牛)
1.切片的数据结构
由图可以看到切片的底层实际上是一个数据结构,包含指向数组的指针,len,cap
len是切片可以访问的长度,当用for遍历的时候,打印出来的个数就是len的长度
cap最大可以是底层数组的容量-array指针的位置,当底层数组容量不够的时候,将进行扩容
并且两个切片还允许指向同一个底层数组,这个时候如果对一个切片进行操作,会改变另外一个切片的值**(共享存储空间)**
func TestTwoSliceChange(t *testing.T) {
array := [6]int{10, 20, 30, 40, 50, 60}
sliceA := array[2:5:5]
sliceB := array[1:3:5]
fmt.Println(sliceA)
fmt.Println(sliceB)
sliceA[0] = 999
fmt.Println(sliceA)
fmt.Println(sliceB)
/*打印结果
[30 40 50]
[20 30]
[999 40 50]
[20 999]
*/
}
2.切片的扩容(1.18)
go的切片扩容机制(小米+1, 腾讯,知乎,字节,伴鱼)
在以前的版本中:
-
如果新申请容量比两倍原有容量大,那么扩容后容量大小 等于 新申请容量
-
如果原有 slice 长度小于 1024, 那么每次就扩容为原来的 2 倍
-
如果原 slice 大于等于 1024, 那么每次扩容就扩为原来的 1.25 倍
在1.18的版本中:
- 如果新申请容量比两倍原有容量大,那么扩容后容量大小等于新申请容量
- 如果原有slice长度小于256,那么每次就扩容为原来的2倍
- 如果原有slice大于等于256,那么每次扩容就扩为原来的1.25倍+3/4*256(也就是1.25倍再加192)
测试代码如下
//测试cap的变化规律
//在512之前都是两倍的增长
func TestCap(t *testing.T) {
var r = 10000
arr := make([]int, 0, 1)
c := cap(arr)
for i := 0; i < r; i++ {
arr = append(arr, 1)
if n := cap(arr); n != c {
fmt.Printf("%d->%d %f倍\n", i, n, float64(n)/float64(i))
c = n
}
}
}
扩容 源代码在 go/src/slice.go中
func growslice(et *_type, old slice, cap int) slice {
//主要代码
newcap := old.cap
doublecap := newcap + newcap
//如果cap比两倍还大,直接将新容量等于cap
if cap > doublecap {
newcap = cap
} else {
//否则进行扩容
const threshold = 256
//如果旧容量小于256,直接进行翻倍
if old.cap < threshold {
newcap = doublecap
} else {
//否则进行一个for循环进行扩容
//扩容策略是
// 检查 0 < newcap 去预防 扩容溢出和无限的循环
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
//从两倍扩容变成一个1.25倍扩容然后再加192,这个函数给了两者一个平滑的过渡()
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
//如果扩容会导致溢出,那直接将新容量等于cap
if newcap <= 0 {
newcap = cap
}
}
}
但是就这样结束了吗,我们将结果打印出来
1->2 2.000000倍
2->4 2.000000倍
4->8 2.000000倍
8->16 2.000000倍
16->32 2.000000倍
32->64 2.000000倍
64->128 2.000000倍
128->256 2.000000倍
256->512 2.000000倍
512->848 1.656250倍
848->1280 1.509434倍
1280->1792 1.400000倍
1792->2560 1.428571倍
2560->3408 1.331250倍
3408->5120 1.502347倍
5120->7168 1.400000倍
7168->9216 1.285714倍
9216->12288 1.333333倍
我们主要看256这个临界点的值
256*1.25+192=512 确实没什么问题
但是下一个就出现问题了
512*1.25+192=832
答案应该是832才对,但是扩容的结果确实848。
后来才发现扩容源码不仅这点,下面的操作也会对容量进行改变
//关键代码
//计算新的容量所需要的空间
newlenmem = uintptr(cap) * goarch.PtrSize
//关键这步,内存页对齐,golang 中pagesize 为8kB
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
///判断是否溢出
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
//修改后,newcap变成了842
newcap = int(capmem / goarch.PtrSize)
也就是说主要是内存对齐导致容量与预期的有所差异