前言
简单来说,切片就是一种简化版的动态数组。因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,因此在Go代码中数组使用的并不多。而切片则使用得相当广泛。
Slice源码分析
注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。
在看源码之前,我们先来看一个简单的例子
func main() {
slice:=make([]int,0) //第6行
slice=append(slice,1)//第7行
}
通过make创建一个长度为0,容量为0的slice,接着通过append函数向slice追加元素
接下来,我们就来分析slice创建以及追加的过程
由于append() 是内置函数,所以我们只能通过查看它的汇编语言才能知道append() 发生了什么
内置函数:Go 语言拥有一些不需要进行导入操作就可以使用的内置函数
用以下命令查看的Go语言程序对应的伪汇编代码:
go tool compile -S slice.go
其中go tool compile命令用于调用Go语言提供的底层命令工具,其中-S参数表示输出汇编格式。
...
0x0028 00040 (slice.go:6) PCDATA $2, $1
0x0028 00040 (slice.go:6) PCDATA $0, $0
0x0028 00040 (slice.go:6) LEAQ type.int(SB), AX
0x002f 00047 (slice.go:6) PCDATA $2, $0
0x002f 00047 (slice.go:6) MOVQ AX, (SP)
0x0033 00051 (slice.go:6) XORPS X0, X0
0x0036 00054 (slice.go:6) MOVUPS X0, 8(SP)
0x003b 00059 (slice.go:6) CALL runtime.makeslice(SB)
0x0040 00064 (slice.go:6) PCDATA $2, $1
0x0040 00064 (slice.go:6) MOVQ 24(SP), AX
0x0045 00069 (slice.go:7) PCDATA $2, $2
0x0045 00069 (slice.go:7) LEAQ type.int(SB), CX
0x004c 00076 (slice.go:7) PCDATA $2, $1
0x004c 00076 (slice.go:7) MOVQ CX, (SP)
0x0050 00080 (slice.go:7) PCDATA $2, $0
0x0050 00080 (slice.go:7) MOVQ AX, 8(SP)
0x0055 00085 (slice.go:7) XORPS X0, X0
0x0058 00088 (slice.go:7) MOVUPS X0, 16(SP)
0x005d 00093 (slice.go:7) MOVQ $1, 32(SP)
0x0066 00102 (slice.go:7) CALL runtime.growslice(SB)
0x006b 00107 (slice.go:7) PCDATA $2, $1
0x006b 00107 (slice.go:7) MOVQ 40(SP), AX
0x0070 00112 (slice.go:7) MOVQ 48(SP), CX
0x0075 00117 (slice.go:7) MOVQ 56(SP), DX
0x007a 00122 (slice.go:7) MOVQ $1, (AX)
...
其中,我们只需要关注CALL指令,看它调用了什么。
0x003b 00059 (slice.go:6) CALL runtime.makeslice(SB)
slice.go第六行,调用了makeslice函数,则makeslice函数是用于初始化slice的
0x0066 00102 (slice.go:7) CALL runtime.growslice(SB)
同理,growslice函数是用于追加元素的
makeslice
接下来,我们看看在go的sdk里面slice是如何实现这些函数的
初始化makeslice
runtime\slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 的底层结构定义非常直观,指向底层数组的指针,当前长度 len 和当前 slice 的 cap。
再来看看makeslice函数
func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 获取需要申请的内存大小
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 分配内存
// 小对象从当前P 的cache中空闲数据中分配
// 大的对象 (size > 32KB) 直接从heap中分配
return mallocgc(mem, et, true)
}
实在是太简单了,没啥可说的。mallocgc 函数会根据申请的内存大小,去对应的内存块链表上找合适的内存来进行分配,是 Go 自己改造的 tcmalloc 那一套。
数据扩容growslice
func growslice(et *_type, old slice, cap int) slice {
//扫描内存
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
//容量判断
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
//如果存储的类型空间为0, 比如说 []struct{}, 数据为空,长度不为空
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
//如果新的容量大于旧的两倍,则直接扩容到新的容量
if cap > doublecap {
newcap = cap
} else {
//当新的容量不大于旧的两倍
// 如果旧长度小于1024,那扩容到旧的两倍
if old.len < 1024 {
newcap = doublecap
} else {
//否则扩容到旧的1.25倍
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
//检查newCap是否溢出
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 为了加速计算
// 对于不同的slice元素大小,选择不同的计算方法
// 获取需要申请的内存大小。
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):
//如果是二的倍数,用位移运算
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
//默认用除法
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
// 判断是否会溢出
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
//为新的切片开辟容量为capmem的地址空间
p = mallocgc(capmem, nil, false)
// 仅清除不会被覆盖的部分。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 注意:不能使用rawmem(这样可以避免内存清零),因为GC可以扫描未初始化的内存。
p = mallocgc(capmem, et, true)
if writeBarrier.enabled {
//因为我们知道目标切片p,所以仅将指针隐藏在old.array中
//仅包含nil指针,因为在分配过程中已将其清除。
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
//数据拷贝
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
扩容规则:
- 新的容量大于旧的2倍,直接扩容至新的容量
- 当新的容量不大于旧的2倍
- 当旧的长度小于1024,扩容至旧的2倍
- 扩容至旧的1.25倍
slice 扩容必然会导致内存拷贝,如果是性能敏感的系统中,尽可能地提前分配好 slice 是较好的选择。
数据拷贝slicecopy
func slicecopy(to, fm slice, width uintptr) int {
//从slice长度为0或者到slice长度为0
if fm.len == 0 || to.len == 0 {
return 0
}
n := fm.len
if to.len < n {
n = to.len
}
//长度为0,直接返回
if width == 0 {
return n
}
//分析内存
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(slicecopy)
racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
}
if msanenabled {
msanwrite(to.array, uintptr(n*int(width)))
msanread(fm.array, uintptr(n*int(width)))
}
size := uintptr(n) * width
if size == 1 {
// 直接内存拷贝
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}
总结
- 尽量对切片设置初始容量值以避免扩容,slice 扩容必然会导致内存拷贝
- 切片是一个结构体,保存着切片的容量,实际长度以及数组的地址
- 多个slice 可能指向同一个底层数组
- slice 是按值传递的,Go里只有传值(值传递)