golang的切片slice
GET知识点
- 切片的扩容原理和设计
- 切片容量的作用
- 了解golang的内存对齐
- 不指定容量,则默认长度和容量相等
- 合理的指定容量,避免频繁的内存分配和数据copy。
- 代码反编译,或者 下载dlv源代码调试工具可以起到学习辅助作用
go tool compile -S make.go # golang的代码的反编译
go install github.com/go-delve/delve/cmd/dlv@latest
参考链接
源代码
- 代码基于 go1.17.8分析
- 系统-Mac
数据结构
type slice struct {
array unsafe.Pointer // 指向底层数组指针
len int // 长度
cap int // 容量
}
- array 指向底层数组的指针
- len,底层数组中元素的个数,即允许访问的个数
- cap,容量,底层数组的size,当 len+要添加的元素个数 大于 cap是会发生扩容。
扩容机制(1)
func growslice(et *_type, old slice, cap int) slice {
// 代码省略.....
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap { // cap:所需的最小容量 > old*2,则直接使用cap
newcap = cap
} else {
if old.cap < 1024 { // 2倍的分配需要有个限制,过多的分配可能会导致资源的浪费。所以这里以1024为分界线
newcap = doublecap
} else { // 合理配置一个所需的最小容量,已1024为分界线。
for 0 < newcap && newcap < cap { // 不过多分配,
newcap += newcap / 4 // 预估容量是扩容前容量的1.25倍,即以0.25的倍数增加
}
if newcap <= 0 {
newcap = cap
}
}
}
}
cap > doublecap
0、未定义slice,cap大小,首次append的时候,比如:,var nums []int,
0、当addEle的元素大于 old*2的时候,比如: append(nums,添加元素个数的超过len(nums)的2倍多)
old.cap < 1024
0、以1024作为2倍的分配界限,避免分配的过多导致资源浪费,
0、如果超过1024则,每次以1.25倍的增量进行分配,直至分配出一个,minimum capacity,
扩容机制(2)
内存对齐,根据元素类型大小,重新计算所需的newcap数量,
这里的et.size指的是,元素类型大小,
roundupsize
向上取整,取最接近的大小
func growslice(et *_type, old slice, cap int) slice {
// 代码省略.....
switch {
case et.size == sys.PtrSize: // 8
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)
}
}
// runtime/sizeclasses.go
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
扩容机制(3)
func growslice(et *_type, old slice, cap int) slice {
// 代码省略.....
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
0、分配内存,
0、将老数组内容copy到新分配好的数组中
0、返回新的slice
例子分析
1、了解slice扩容
func main() {
nums := make([]int, 0)
nums = append(nums, 1)
// [1] 1 1
fmt.Println(nums, len(nums), cap(nums))
}
- 使用 dlv或者 go tool compile -S main.go(反编译)代码,只需要关注下面这个2个关键的
runtime.makeslice(SB) // 初始化切片,并返回
runtime.growslice(SB) // 切片的扩容
-
扩容的触发逻辑,容量 < 长度+添加元素个数则 触发 growslice逻辑
-
否则无需扩容,直接从len开始往后+1,赋值添加的元素
// cmd/compile/internal/walk/builtin.go
// init {
// s := src
// const argc = len(args) - 1
// if cap(s) - len(s) < argc {
// s = growslice(s, len(s)+argc)
// }
// n := len(s)
// s = s[:n+argc]
// s[n] = a
// s[n+1] = b
// ...
// }
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {}
- growslice分析
num的初始长度和容量是 len=0,cap=0
0、所需的最小容量cap=1,len(nums)+len(1)
0、cap 大于 cap(nums)*2倍,则newcap=1
0、内存对齐的计算,capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
0、capmem = roundupsize(1*8) = 8
0、所以最后所需容量的个数是1。newcap = int(capmem / sys.PtrSize), newcap=int(8/8)= 1
0、分配内存,将老数组内容copy到新分配好的数组中,返回新切片
2、为什么结果是这样?
下面代码执行是不会发生扩容的,已经提交预分配好容量了。
func TestSliceA(t *testing.T) {
numA := make([]string, 0, 5)
numA = append(numA, "a1", "a2")
numsB := append(numA, "b1") // numsB = numA[:len(numA)+1] , numsB[len(nimA)] = "b1"
t.Logf("numA=%v numB=%v\n", numA, numsB)
numA = append(numA, "b2")
t.Logf("numA=%v numB=%v \n", numA, numsB)
}
/*
slice_test.go:15: numA=[a1 a2] numB=[a1 a2 b1]
slice_test.go:18: numA=[a1 a2 b2] numB=[a1 a2 b2]
*/
- numA = append(numA,"a1","a2")
numA初始化容量=5,长度=0,并添加【a1,a2】元素
- numsB := append(numA, "b1")
此时,numA 和numB的都指向了相同的底层数组,不一样就是,两者之间的长度不一样。即能够访问的元素的长度不同。
- numA = append(numA, "b2")
因为numA和numB,共享底层数组,所以在numA添加b2是,将 numsB[2]覆盖掉。
Q&A
1、切片中的_type中的 size 和prtdata是什么?
size:切片类型大小
ptrdata:切片类型中指针的大小
主要是用来判断,分配合适的内存大小。
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
// 代码省略。。。
}
比如:
type User struct {
Age *int
Year int
}
nums := make([]User, 1)
size:16,ptrdata:8
type User struct {
Age *int
Year *int
}
nums := make([]*User, 1)
size:8,ptrdata:16
2、切片的容量的作用
如果不提前指定容量或者长度,则切片可能会发生频繁的,内存分好和值拷贝,影响效率。
用途:在数据拷贝和内存申请消耗与内存占用之间提供一个权衡。
3、slice的growslice调用方
growslice是指有发生扩容在会走这个方法
在执行append的时候,growslice的触发条件是 cap(slice) < len(slice)+ addEleCnt, 切片容量 < 切片长度+ 添加元素个数
注意注释中init的操作
// cmd/compile/internal/walk/builtin.go
// init { 伪代码
// s := src
// const argc = len(args) - 1
// if cap(s) - len(s) < argc {
// s = growslice(s, len(s)+argc)
// }
// n := len(s)
// s = s[:n+argc]
// s[n] = a
// s[n+1] = b
// ...
// }
func walkAppend(n *ir.CallExpr, init *ir.Nodes, dst ir.Node) ir.Node {
fn := typecheck.LookupRuntime("growslice") // growslice(<type>, old []T, mincap int) (ret []T)
4、空切片和nil
- nil切片:
只是声明不做任何初始化操作
var nums []string
底层内存结构的表现形式:slice的底层array是没有有地址
runtime.slice {array: unsafe.Pointer(0x0), len: 0, cap: 0}
使用场景:用来描述一个不存在的切片
- 空切片,
make或者字面量初始化为空
// make初始化
nums := make([]string,0)
// 字面量初始化
nums2 := []string{}
底层内存结构的表现形式:slice的底层array是有地址
runtime.slice {array: unsafe.Pointer(0xc000040750), len: 0, cap: 0}
使用场景:用来表示空集合是很有用,比如数据库查询,没有查询到数据。
- 使用
两者在使用append,len,cap,都不影响。