没错我就是标题党。
切片是对数组的封装,所以讨论切片前必须对数组有足够的了解。
先来看下面一段代码:
demo1 := [2]uint8{255, 255}
demo2 := [2]uint16{65535, 65535}
demo3 := [2]uint32{4294967295, 4294967295}
demo4 := [2]uint64{18446744073709551615, 18446744073709551615}
fmt.Println("[2]uint8")
fmt.Println(reflect.ValueOf(&demo1[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo1[1]).Pointer())
fmt.Println("[2]uint16")
fmt.Println(reflect.ValueOf(&demo2[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo2[1]).Pointer())
fmt.Println("[2]uint32")
fmt.Println(reflect.ValueOf(&demo3[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo3[1]).Pointer())
fmt.Println("[2]uint64")
fmt.Println(reflect.ValueOf(&demo4[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo4[1]).Pointer())
运行结果为:
[2]uint8
824633802944
824633802945
[2]uint16
824633802948
824633802950
[2]uint32
824633802952
824633802956
[2]uint64
824633802960
824633802968
一个uint8占一个字节,所以第二个元素的指针值比第一个大1,一个uint16占两个字节,第二个元素指针值比第一个大2,以此类推。所以golang的数组在内存里面是一片连续的空间,这样在取值的时候,指针只需要依次移动8的倍数位(一字节等于8位)就能得到相应的值。
数组变量代表的是整个数组而不是指向首元素地址的指针,所以这玩意儿在变量间赋值的时候是值传递而不是引用传递,来看看下面的代码:
demo1 := [1]uint{1}
demo2 := demo1
fmt.Println(reflect.ValueOf(&demo1[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo2[0]).Pointer())
运行结果:
824633802944
824633802952
于是,程序运行的时候你电脑的内存里面存了两个值完全相同的数组。
数组变量代表的整个数组,但变量存储于栈区数组存储于堆区,因此这个变量总得指向啥东西吧:
demo := [2]uint{1,2}
fmt.Println(reflect.ValueOf(&demo).Pointer())
fmt.Println(reflect.ValueOf(&demo[0]).Pointer())
fmt.Println(reflect.ValueOf(&demo[1]).Pointer())
运行结果:
824633802944
824633802944
824633802952
OK,懂c语言的朋友就知道了,这玩意儿和c的数组就是一样的机制,栈区的变量指向堆区数组中的第一个元素,但千万要注意与c不同的是赋值的时候是值传递而不是引用传递。
接下来说说切片,不知道这玩意儿为什么叫切片不叫切块。
相信看过文档的朋友都对这张图非常熟悉:
那个ptr就是指针,指向内存中一片连续的空间,也就是前文说的数组。切片使用了结构体来对数组进行封装,那个结构体长这德行:
也许有些朋友自己想看一眼源码——/$goroot$/src/runtime/slice.go第13行。
初始化一个切片的时候,操作系统会分配相应大小的连续空间给它。当元素填满了切片后,必须进行空间扩容,但这并不是简单地在后面添加就完事了,必须重新申请一块更大的空间,把数据从旧空间复制到新空间,然后销毁旧空间。看看下面代码:
demo := []int{1}
fmt.Println("扩容前cap:",cap(demo),"一号元素地址:", reflect.ValueOf(&demo[0]).Pointer())
demo = append(demo, 1)
fmt.Println("扩容后cap:",cap(demo),"一号元素地址:", reflect.ValueOf(&demo[0]).Pointer())
运行结果:
扩容前cap: 1 一号元素地址: 824634163304
扩容后cap: 2 一号元素地址: 824634163376
网上相当多的文章说切片的扩容机制是小于1024的时候扩为原来的两倍,大于时扩为原来的1.25倍,看起来说得头头是道,好的那咱就来试试:
demo := make([]int, 100)
fmt.Println("扩容前容量:",cap(demo))
demo = append(demo, 1)
fmt.Println("扩容后容量:",cap(demo))
运行结果:
what the fffffffuc。。。。???!!!!
100 * 2 = 224!!!!
1024以下的不好使我们来试试1024以上的,这次玩个大的来个10000容量:
demo := make([]int, 10000)
fmt.Println("扩容前容量:",cap(demo))
demo = append(demo, 1)
fmt.Println("扩容后容量:",cap(demo))
运行结果
10000 * 1.25 = 13312。。。???
看来其他文章里面说的不大好使。
那当然要不好使了,要不然这片文章怎么能说另有玄机。
现在咱去研究切片的源码。切片的扩容函数也在slice.go里面,第76行的growslice()就是了。小于1024扩为两倍大于扩为1.25倍这个算法也不能说错,因为函数中有这么一段代码:
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
}
}
}
但是坑father的来了,除了这段外还有下面这一段:
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)
}
这个switch分支case的东西我搞了好大半天才明白,它是类型大小,即一个类型所占的空间。可别想着在这里用fmt.Pringln()打印出这个类型大小,一导入fmt这个包,就会得到这几个字:import cycle not allowed。
别问我怎么知道的,我是不可能告诉你们的。
所以,要得到这个类型大小……伟大的反射登场吧!
demo1 := true
fmt.Println("bool: ", reflect.TypeOf(demo1).Size())
demo2 := 'a'
fmt.Println("rune: ", reflect.TypeOf(demo2).Size())
demo3 := 1
fmt.Println("int: ", reflect.TypeOf(demo3).Size())
demo4 := "a"
fmt.Println("string: ", reflect.TypeOf(demo4).Size())
Let us see see 运行结果。
bool: 1
rune: 4
int: 8
string: 16
接下来就是roundupsize()函数,知道它是啥意思么,不知道没事我也不知道,咱点开看看呗。
好的这是msize.go里面的函数。这个go文件当头一句注释就是“Malloc small size classes.”
OK,现在成功扯上了内存管理。
roundupsize()函数长这么个德行:
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
} else {
return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
}
}
if size+_PageSize < size {
return size
}
return round(size, _PageSize)
}
一切的玄机就在这个函数里面,它的作用根据申请空间大小返回实际分配空间大小。如果申请空间小于_MaxSmallSize也就是32678也就是32K的时候,它利用smallSizeDiv、smallSizeMax、largeSizeDiv计算索引来从size_to_class和class_to_size中查值返回。
让什么smallSizeDiv、smallSizeMax、largeSizeDiv、size_to_class和class_to_size见鬼去吧,这么说谁听得懂啊,这些什么鬼玩意儿啊!
下面咱看看这张表。
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
那个啥,请只关注bytes/obj列,其它列我懒得删了,咳咳。
这一张表在/$goroot$/src/runtime/sizeclass.go里面。
现在咱把bytes/obj列的每两个数据看成(8~16],(16~32]这种形式,当某个数据落入这个区间时返回区间最大值。讲人话就是你给他9或10它给你16,给他17或18它给你32。以此类推。
smallSizeDiv、smallSizeMax、largeSizeDiv、size_to_class*和class_to_size的作用就是上一个段落。
如果申请空间大于32K,那就会使用/$goroot$/src/runtime/stubs.go里面round()函数来计算实际返回空间。
// round n up to a multiple of a. a must be a power of 2.
func round(n, a uintptr) uintptr {
return (n + a - 1) &^ (a - 1)
}
这个函数的作用是将n转化为a的倍数,a必须为2的幂数。
位运算牛逼!不接受任何辩驳!
如果我来写这个函数,我大概只能想到暴力搜索法。真是野蛮。
好了,现在咱重新来看看这个例子:
demo := make([]int, 100)
fmt.Println("扩容前容量:",cap(demo))
demo = append(demo, 1)
fmt.Println("扩容后容量:",cap(demo))
int的类型大小是8,因此在switch分支中执行了这一段代码:
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)
sys.PtrSize,根据名称来看就是pointer size,指针大小,在我的机器上指针大小是8。
这里面只需要关注两个参数:capmem和newcap。开始执行的时候,经过了小于1024扩为两倍的计算,newcap的值现在为200,因此传入roundupsize()函数的值为1600,根据那张表,得到函数返回值为1796。
算算1796除以8后向下取整的值吧,是不是224?嘿嘿!
至于为什么要向下取整,相信朋友们不会忘了浮点型转化为整型是怎么玩的。
所以,切片的扩容并不只是将其转化为2倍或者1.25倍就完事了,接下来还要根据切片元素的类型大小来二次扩容。这一部分已经涉及到golang的内存分配,如果想要进一步深入,还需要到slice.go和msize.go文件中去看源码,它们都在/$goroot$/src/runtime文件夹下(好吧我承认这里不写出来的原因是懒得写了hhhhh,但看一遍源码总比单看我这文章好),当然配合我这片文章来看感觉会更丝滑,因为我已经把那个类型大小和内存分配原理给解决了。
PS:补充下当类型大小是2的幂数时会执行这一段代码:
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)
其中shift的值是log2(et.size) & 63或者log2(et.size) & 31。