Slice本质是什么
Slice是golang中一个可变长度的数组,我们知道可变长度的数组都是通过固定长度的数组进行扩容实现的,比如Java中的ArrayList。
Slice的本质是一个结构体如下:
//在栈中的slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
//在堆中的slice
type notInHeapSlice struct {
array *notInHeap
len int
cap int
}
空间分配
通过代码观察空间是翻倍
package main
import "fmt"
func main() {
memoryCap()
}
func memoryCap() {
s := []int{}
for i := 0; i < 16; i++ {
fmt.Printf("分配内存的大小:%d \n", cap(s))
fmt.Println(s)
s = append(s, i)
}
}
$ go run var/slice.go
分配内存的大小:0
[]
分配内存的大小:1
[0]
分配内存的大小:2
[0 1]
分配内存的大小:4
[0 1 2]
分配内存的大小:4
[0 1 2 3]
分配内存的大小:8
[0 1 2 3 4]
分配内存的大小:8
[0 1 2 3 4 5]
分配内存的大小:8
[0 1 2 3 4 5 6]
分配内存的大小:8
[0 1 2 3 4 5 6 7]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9 10]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9 10 11]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9 10 11 12]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9 10 11 12 13]
分配内存的大小:16
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14]
其实真实的策略是
if Old*2 < Need { 直接扩容到Need的大小 }else{ if Old < 1024 { 翻倍 }else{ 1.25倍 } }
源码解析
0x0000 00000 (var/slice_runtime.go:5) TEXT "".main(SB), ABIInternal, $152-0
0x0000 00000 (var/slice_runtime.go:5) MOVQ (TLS), CX
0x0009 00009 (var/slice_runtime.go:5) LEAQ -24(SP), AX
0x000e 00014 (var/slice_runtime.go:5) CMPQ AX, 16(CX)
0x0012 00018 (var/slice_runtime.go:5) PCDATA $0, $-2
0x0012 00018 (var/slice_runtime.go:5) JLS 384
0x0018 00024 (var/slice_runtime.go:5) PCDATA $0, $-1
0x0018 00024 (var/slice_runtime.go:5) SUBQ $152, SP
0x001f 00031 (var/slice_runtime.go:5) MOVQ BP, 144(SP)
0x0027 00039 (var/slice_runtime.go:5) LEAQ 144(SP), BP
0x002f 00047 (var/slice_runtime.go:5) FUNCDATA $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x002f 00047 (var/slice_runtime.go:5) FUNCDATA $1, gclocals·3e41b1273e091f6792499a04c3554d8f(SB)
0x002f 00047 (var/slice_runtime.go:5) FUNCDATA $3, "".main.stkobj(SB)
0x002f 00047 (var/slice_runtime.go:6) LEAQ type.int(SB), AX
0x0036 00054 (var/slice_runtime.go:6) MOVQ AX, (SP)
0x003a 00058 (var/slice_runtime.go:6) XORPS X0, X0
0x003d 00061 (var/slice_runtime.go:6) MOVUPS X0, 8(SP)
0x0042 00066 (var/slice_runtime.go:6) PCDATA $1, $0
0x0042 00066 (var/slice_runtime.go:6) `CALL runtime.makeslice(SB)` //创建slice
0x0047 00071 (var/slice_runtime.go:6) MOVQ 24(SP), AX
0x004c 00076 (var/slice_runtime.go:7) LEAQ type.int(SB), CX
0x0053 00083 (var/slice_runtime.go:7) MOVQ CX, (SP)
0x0057 00087 (var/slice_runtime.go:7) MOVQ AX, 8(SP)
0x005c 00092 (var/slice_runtime.go:7) XORPS X0, X0
0x005f 00095 (var/slice_runtime.go:7) MOVUPS X0, 16(SP)
0x0064 00100 (var/slice_runtime.go:7) MOVQ $1, 32(SP)
0x006d 00109 (var/slice_runtime.go:7) `CALL runtime.growslice(SB)` //扩容slice
0x0072 00114 (var/slice_runtime.go:7) MOVQ 40(SP), AX
0x0077 00119 (var/slice_runtime.go:7) MOVQ 56(SP), CX
0x007c 00124 (var/slice_runtime.go:7) MOVQ CX, "".slice.cap+72(SP)
0x0081 00129 (var/slice_runtime.go:7) MOVQ 48(SP), DX
0x0086 00134 (var/slice_runtime.go:7) MOVQ $1, (AX)
0x008d 00141 (var/slice_runtime.go:8) MOVQ AX, (SP)
0x0091 00145 (var/slice_runtime.go:7) LEAQ 1(DX), AX
0x0095 00149 (var/slice_runtime.go:7) MOVQ AX, "".slice.len+64(SP)
源码在runtime/slice.go
简单来说,makeslice函数的工作主要就是计算slice所需内存大小,然后调用mallocgc进行内存的分配。计算slice所需内存又是通过MulUintptr来实现的,MulUintptr的源码我也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,如果内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外如果传入长度小于0或者长度小于容量,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 {
// 简单来说,makeslice函数的工作主要就是计算slice所需内存大小,然后调用mallocgc进行内存的分配。计算slice所需内存又是通过MulUintptr来实现的,MulUintptr的源码我也已经贴出,主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间,如果内存溢出,或者计算出的内存大小大于最大可分配内存,MulUintptr的overflow会返回true,makeslice就会报错。另外如果传入长度小于0或者长度小于容量,makeslice也会报错
// 可以查看这个issue golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
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 {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 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 sys.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 == 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)
}
// 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作为参数时
如果作为参数传到方法里面的话,方法对slice的修改也会导致外部的slice发生改变。 主要是因为底层的实现是通过数组,slice值维护了一个数组指针
package main
import "fmt"
func main() {
s := []int{-1, -1}
fmt.Println(s)
sliceChange(s)
fmt.Println(s)
}
func sliceChange(s []int) {
s[0] = 1
s[1] = 1
s = append(s, 2)
fmt.Println("change:", s)
}
如果方法对slice有修改的话,正确的做法是使用copy
package main
import "fmt"
func main() {
s2 := make([]int, 2)
s := []int{-1, -1}
fmt.Println(s)
copy(s2, s)
fmt.Println(s2)
sliceChange(s2)
fmt.Println(s)
}
func sliceChange(s []int) {
s[0] = 1
s[1] = 1
s = append(s, 2)
fmt.Println("change:", s)
}