golang slice浅入浅出(一)
参考文章:深度解密go语言之slice
go slice 浅入浅出!
一、slice的运行时结构
golang中的slice是经常会使用到的结构类型,那么先简单看一下和它相关的源码!
//$GOROOT\src\runtime\slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
从runtime包这里可以看到slice的结构,一个名为{array}(数组)的{unsafe.Pointer}(指针)。这说明slice的底层其实也是数组。另外slice还包含{len}(长度)和{cap}(容量),分别表示slice目前和最大的长度。
二、slice的扩容
slice在go中可以说是会经常接触!而另一个数据类型array却非常少,原因就是array长度不可变,不同长度的array就是不同的类型,无法通用。slice就解决了这个问题,但是底层仍然用的是array!今天就先来看看slice是如何扩容的!
首先,咱们写一个main函数,创建一个slice,指定长度和容量都为1,然后调用append添加一个数。这个就写好了。
package main
func main() {
s := make([]int, 1, 1)
s = append(s, 4)
}
接下来,在main.go文件所在目录命令行用go tool compile命令查看汇编码:
go tool compile -S .\main.go
结果如下,其实我也大部分看不懂,没关系!咱们看到runtime.growslice就可以了,见名知意,这是运行时调用slice扩容的函数。
"".main STEXT size=84 args=0x0 locals=0x50 funcid=0x0 align=0x0
0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $80-0
0x0000 00000 (main.go:3) CMPQ SP, 16(R14)
0x0004 00004 (main.go:3) PCDATA $0, $-2
0x0004 00004 (main.go:3) JLS 77
0x0006 00006 (main.go:3) PCDATA $0, $-1
0x0006 00006 (main.go:3) SUBQ $80, SP
0x000a 00010 (main.go:3) MOVQ BP, 72(SP)
0x000f 00015 (main.go:3) LEAQ 72(SP), BP
0x0014 00020 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0014 00020 (main.go:5) MOVQ $0, ""..autotmp_1+64(SP)
0x001d 00029 (main.go:5) LEAQ type.int(SB), AX
0x0024 00036 (main.go:5) LEAQ ""..autotmp_1+64(SP), BX
0x0029 00041 (main.go:5) MOVL $1, CX
0x002e 00046 (main.go:5) MOVQ CX, DI
0x0031 00049 (main.go:5) MOVL $2, SI
0x0036 00054 (main.go:5) PCDATA $1, $0
0x0036 00054 (main.go:5) CALL runtime.growslice(SB)
0x003b 00059 (main.go:5) MOVQ $4, 8(AX)
0x0043 00067 (main.go:6) MOVQ 72(SP), BP
0x0048 00072 (main.go:6) ADDQ $80, SP
0x004c 00076 (main.go:6) RET
0x004d 00077 (main.go:6) NOP
0x004d 00077 (main.go:3) PCDATA $1, $-1
0x004d 00077 (main.go:3) PCDATA $0, $-2
0x004d 00077 (main.go:3) CALL runtime.morestack_noctxt(SB)
0x0052 00082 (main.go:3) PCDATA $0, $-1
0x0052 00082 (main.go:3) JMP 0
找到$GOROOT/src/runtime/slice.go中的growslice函数,下面是全文!非常的多判断~不过我既然要浅入浅出就不考虑复杂的情况啦!感兴趣的可以自己细读,下翻挑重点看。(源码版本:go1.18.4 windows/amd64)
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if asanenabled {
asanread(old.array, uintptr(old.len*int(et.size)))
}
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
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 {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
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.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
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 == goarch.PtrSize:
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if goarch.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)
}
// The check of overflow in addition to capmem > maxAlloc is needed
// to prevent an overflow which can be used to trigger a segfault
// on 32bit architectures with this example program:
//
// type T [1<<27 + 1]int64
//
// var d T
// var s []T
//
// func main() {
// s = append(s, d, d, d, d)
// print(len(s), "\n")
// }
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
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}
}
重点一:扩容分两步,一步根据需要扩大的容量进行判断,第二步进行内存对齐
第一步扩容如下,接下来我进行文字解释一下。
①如果扩容容量大于旧容量的两倍,就暂时决定容量为需要扩容容量(还实际未设置,只是利用临时变量保存这个值,下同)
②如果slice的{容量}(cap)小于256,就暂时决定容量为原先的两倍。
③如果slice的{容量}(cap)大于256,就使旧容量循环加上(旧容量+3*256)/4,直到这个值大于需要扩容的容量。后暂时决定这个值为容量。
④如果经过前面的操作导致暂时决定容量超出限制时,就暂时决定容量为需要扩容的容量。
func growslice(et *_type, old slice, cap int) slice {
//......
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
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.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
//......
}
第二步:内存对齐
啥是内存对齐?
CPU是将内存看做一块一块读取的。块的大小可以是2,4,8,16字节大小。就像一格格的网格,放不满的格子就会留出空白,这就是内存对齐。
为啥要内存对齐?
我的理解是,CPU如果读取到没对齐的内存,需要付出额外的工作去完成正常的读取。所以内存对齐是以空间换时间的做法~
当然,网上也有说法是因为硬件平台可能不可以访问没有进行内存对齐的数据。
了解了内存对齐,下面就比较好看懂啦。
下面的代码是一个switch,其中et.size表示的是slice保存结构所占的字节。
重点是capmem和newcap两个变量!capmem会影响到newcap,newcap就是最终的growslice函数最终会确定的{容量}(cap)。
①如果所占字节为1,那么就无需进行内存对齐了。(再多一嘴,就像是1*1的网格,每个都刚好放满,不会流出空白)
②如果不是的话,根据各种情况进行一系列的对齐操作,保证扩容后的容量分配适配当前机器。通过roundupsize函数计算(当前slice的暂时决定容量)*(类型所占字节)保证所占的内存格子是符合内存对齐的(感兴趣的话可以去看源码)
(goarch.PtrSize是当前机器表示指针所需要的字节,具体值是 4 << (^uintptr(1) >> 63))
(isPowerOfTwo返回:x&(x-1) == 0,作用是删去x的最后一个1后是否等于0)
func growslice(et *_type, old slice, cap int) 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 == goarch.PtrSize:
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if goarch.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)
}
//......
}
//附:部分中间函数/变量
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
const (
_MaxSmallSize = 32768
smallSizeDiv = 8
smallSizeMax = 1024
largeSizeDiv = 128
_NumSizeClasses = 68
_PageShift = 13
)
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
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}
先写到这里,关于slice的其他问题之后有时间再更新!