https://blog.csdn.net/nyist_zxp/article/details/111425091
https://blog.csdn.net/weixin_37509194/article/details/112001014
https://blog.csdn.net/qq_43971008/article/details/105385434
一、源码
Version : go1.15.6 src/runtime/slice.go
//go1.15.6 源码 src/runtime/slice.go func growslice(et *_type, old slice, cap int) slice { //省略部分判断代码 //计算扩容部分 //其中,cap : 所需容量,newcap : 最终申请容量 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 } } } //省略部分判断代码 }
二、原理
1. 如果当前所需容量 (cap) 大于原先容量的两倍 (doublecap),则最终申请容量(newcap)为当前所需容量(cap);
2. 如果<条件1>不满足,表示当前所需容量(cap)不大于原容量的两倍(doublecap),则进行如下判断;
3. 如果原切片长度(old.len)小于1024,则最终申请容量(newcap)等于原容量的两倍(doublecap);
4. 否则,最终申请容量(newcap,初始值等于 old.cap)每次增加 newcap/4,直到大于所需容量(cap)为止,然后,判断最终申请容量(newcap)是否溢出,如果溢出,最终申请容量(newcap)等于所需容量(cap);
数组
切片是对数组的封装,所以讨论切片前必须对数组有足够的了解。
先来看下面一段代码:
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))
运行结果:
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 } } }
但是除了这段外还有下面这一段:
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。
例子: