切片
也许有些朋友自己想看一眼源码——/$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 一号元素地址: 1374390755336
扩容后cap: 2 一号元素地址: 1374390755376
网上相当多的文章说切片的扩容机制是小于1024的时候扩为原来的两倍,大于时扩为原来的1.25倍,看起来说得头头是道,好的那咱就来试试:
func main() {
demo := make([]int, 100)
fmt.Println("扩容前容量:", cap(demo))
demo = append(demo, 1)
fmt.Println("扩容后容量:", cap(demo))
}
运行结果:
扩容前容量: 100
扩容后容量: 224
100 * 2 = 224!!!! 1024以下的不好使我们来试试1024以上的:
demo := make([]int, 10000)
fmt.Println("扩容前容量:",cap(demo))
demo = append(demo, 1)
fmt.Println("扩容后容量:",cap(demo))
运行结果:
扩容前容量: 10000
扩容后容量: 13312
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的东西,它是类型大小,即一个类型所占的空间。
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())
运行结果:
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中查值返回。
下面咱看看这张表。
// 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%
这一张表在/$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,那就会使用//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文件夹下
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。