震惊!golang切片的扩容居然另有玄机,朋友圈都传疯了! - Go语言中文社区

震惊!golang切片的扩容居然另有玄机,朋友圈都传疯了!


没错我就是标题党。

数组

切片是对数组的封装,所以讨论切片前必须对数组有足够的了解。
先来看下面一段代码:

	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。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_43971008/article/details/105385434
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢