slice的数据结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
当slice的len==cap后,再向slice中追加元素时,会发生扩容
扩容遵循如下规则:(但结果不完全遵循此规则)
- 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
- 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
- 如果扩容后的大小仍不能满足,那么直接扩容到所需的容量
在编译过程的中间代码生成阶段,会有一个函数来处理你调用的append
// append(slice, 1, 2, 3)
ptr, len, cap := slice //从slice中取出三个属性
newlen := len + 3 //预先计算追加后的长度,以用来和容量比较
if newlen > cap { //追加后的长度将大于容量,需要进行扩容
ptr, len, cap = growslice(slice, newlen) //扩容的函数,会新开辟一片空间
newlen = len + 3 //更新len
}
// 对实际的内存赋值
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
// 返回处理后的切片
return makeslice(ptr, newlen, cap)
扩容函数如下,扩容过后,原来的slice将被丢弃
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 {
for newcap < cap {
newcap += newcap / 4
}
}
}
// ……
// 如下两行做了内存对齐的操作,是经常导致结果不符合预期的原因,请记住这里
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
roundupsize函数如下
在大小为1, 8(64位机器), 和2的倍数时进行不同的内存对齐操作
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
...
default:
...
}
可以看到有3种分支进行不同的内存对齐,在内存对齐的过程中,向上取整,这里涉及到go的内存分配方式,有一张表如下
// class bytes/obj bytes/span objects waste bytes
// 1 8 8192 1024 0
// 2 16 8192 512 0
// 3 32 8192 256 0
// 4 48 8192 170 32
// 5 64 8192 128 0
// 6 80 8192 102 32
// 7 96 8192 85 32
// 8 112 8192 73 16
// 9 128 8192 64 0
// 10 144 8192 56 128
// 11 160 8192 51 32
// 12 176 8192 46 96
// 13 192 8192 42 128
// 14 208 8192 39 80
// 15 224 8192 36 128
// 16 240 8192 34 32
// 17 256 8192 32 0
// 18 288 8192 28 128
// 19 320 8192 25 192
// 20 352 8192 23 96
// 21 384 8192 21 128
// 22 416 8192 19 288
// 23 448 8192 18 128
// 24 480 8192 17 32
// 25 512 8192 16 0
// 26 576 8192 14 128
// 27 640 8192 12 512
// 28 704 8192 11 448
// 29 768 8192 10 512
// 30 896 8192 9 128
// 31 1024 8192 8 0
// 32 1152 8192 7 128
// 33 1280 8192 6 512
// 34 1408 16384 11 896
// 35 1536 8192 5 512
// 36 1792 16384 9 256
// 37 2048 8192 4 0
// 38 2304 16384 7 256
// 39 2688 8192 3 128
// 40 3072 24576 8 0
// 41 3200 16384 5 384
// 42 3456 24576 7 384
// 43 4096 8192 2 0
// 44 4864 24576 5 256
// 45 5376 16384 3 256
// 46 6144 24576 4 0
// 47 6528 32768 5 128
// 48 6784 40960 6 256
// 49 6912 49152 7 768
// 50 8192 8192 1 0
// 51 9472 57344 6 512
// 52 9728 49152 5 512
// 53 10240 40960 4 0
// 54 10880 32768 3 128
// 55 12288 24576 2 0
// 56 13568 40960 3 256
// 57 14336 57344 4 0
// 58 16384 16384 1 0
// 59 18432 73728 4 0
// 60 19072 57344 3 128
// 61 20480 40960 2 0
// 62 21760 65536 3 256
// 63 24576 24576 1 0
// 64 27264 81920 3 128
// 65 28672 57344 2 0
// 66 32768 32768 1 0
第二列即是分配的内存大小,举个例子
a := []int{1, 2}
a = append(a, 3, 4, 5)
fmt.Println(cap(a)) // 6
在这次向append操作中会发生扩容,预估出需要的容量是5个int大小,如果是64位机器的话,一个int是64位的大小,5个int就是40字节大小,根据向上取整原则,为这个slice对象申请48字节的空间,所以此时的容量为48/8=6。
这种内存对齐的操作在每次扩容是都会发生,所以常常会得出与预期不符的答案,但好在cap莫名其妙的变大了不会造成什么危险的问题,在实际项目的开发中尽量还是提前预估好slice的容量,这样既能避免莫名其妙的结果发生,又能减少频繁的内存申请,内存复制,影响效率
总结扩容规则:
- 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
- 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;
- 如果扩容后的大小仍不能满足,那么新Slice容量等于所需的容量
- 在以上计算完新Slice容量后,交由管理内存的组件申请内存,按照给出的表向上取整进行内存申请,申请出来的内存长度,作为Slice扩容后的容量