从源码分析理解 Go 切片扩容机制,别被大部分水文误导了

一、问题导入

1.1 原问题

在学习 Go 官方提供的语法指南中的 向切片中添加元素小节时运行以下 demo 时发现运行结果最后一行的 cap = 6 无法理解,此时问题可以描述为向长度和容量均为 2 的切片中一次性添加 3 个元素,最终的长度是 5,但是容量为什么是 6?

package main

import "fmt"

func main() {
    
    
	var s []int
	printSlice(s)

	// 添加一个空切片
	s = append(s, 0)
	printSlice(s)

	// 这个切片会按需增长
	s = append(s, 1)
	printSlice(s)

	// 可以一次性添加多个元素
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
    
    
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

上述程序运行结果

len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]	//该行中的 cap=6 无法理解

经过百度大部分水文后,查出来的误人子弟的 Go 语言 1.18 版本中切片的扩容机制如下:【!!!经过后面分析可知,该机制不完整,请读者注意!!!】

一、 如果切片的容量小于1024个元素,那么扩容的时候切片的 cap 就乘以2;一旦元素个数超过1024个元素,增长因子就变成 1.25,即每次增加原来容量的四分之一。
二、如果扩容之后,还没有超过原底层数组的容量,那么切片中的指针指向的位置就还是原数组;如果扩容之后,超过了原底层数组的容量,那么 Go 就会开辟一块新的内存,先把原来元素的值拷贝过来,再进行添加新元素操作,这种情况丝毫不会影响到原数组。

假如按照这个扩容机制,应该是切片的底层数组先扩大到原容量的 2 倍(2 * 2 = 4),发现仍然放不下所有的新元素,然后继续扩大 2 倍(4 * 2 = 8),即 最后一行的 cap 应该等于 8,此时问题仍未得到解决。

1.2 新问题

另外,在查阅资料的过程中又发现了新的问题,运行以下代码时并没有像上述扩容机制原则一描述的那样,运行结果不是 100 * 2 = 200,10000 + 10000 * 1.25 = 11250,而是 22413312

package main

import "fmt"

func main() {
    
    
	t := make([]int, 100)
	printSlice(t)

	t = append(t, 666)
	printSlice(t)
	
	fmt.Println("--------------------")
	
	r := make([]int, 10000)
	printSlice(r)
	
	r = append(r, 888)
	printSlice(r)

}

func printSlice(s []int) {
    
    
	fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
}

上述代码运行结果

len=100 cap=100
len=101 cap=224
--------------------
len=10000 cap=10000
len=10001 cap=13312

二、源码剖析

2.1 growslice()

经过百度以及查阅资料发现负责切片扩容的 api 存在于 %GOROOT%/src/runtime/slice.go 文件中。

// 位于 %GOROOT%/src/runtime/slice.go 文件中

func growslice(et *_type, old slice, cap int) slice {
    
    
    //省略部分判断代码
    
    //计算扩容部分
    //其中 cap : 所需容量(比如上述例子中 2 + 3 = 5),newcap : 最终申请的容量(最终打印的结果 6)
    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
            }
        }
    }
 
    //省略部分判断代码
}

由上述源码可以知道在比较 old.len < 1024 之前首先比较了 cap (所需容量) 与 doublecap (原容量的 2 倍容量) 的关系。结合本例,向原容量为 2 的切片中通过 append() 添加三个新元素,即 cap = 2 + 3 = 5,doublecap = 2 + 2 = 4 (doublecap = newcap + newcap = old.cap + old.cap),cap > doublecap,因此最终申请的容量 newcap = cap = 5此时原来的问题就变成最后一行的 cap 为什么不等于 5 而是等于 6?

经过继续查看 growslice() 函数,发现如下代码,其中 switch 分支 case 中的 et.size 是类型大小,即一个类型所占的空间。本例中切片中元素的数据类型是 int。(int, uint 和 uintptr 在 32 位操作系统上通常为 32 位宽,也即占 4 个字节;在 64 位操作系统上则为 64 位宽,也即占 8 个字节。)

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)
    }

其中 capmem 即是最终申请的容量 newcap 的内存大小,类型为 uintptr

// uintptr is an integer type that is large enough to hold the bit pattern of any pointer
// uintptr 是一个整数类型,不是指针类型,但是足够保存任何一种指针
type uintptr uintptr

sys.PtrSize 是指针的字节大小,在 32 位操作系统中大小为 4 个字节,在 64 位操作系统中大小为 8 个字节。

// %GOROOT%/src/internal/goarch/goarch.go 文件中有 ptrSize 的定义和计算方式

// PtrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant.
// It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit).
const PtrSize = 4 << (^uintptr(0) >> 63)

2.2 roundupsize()

从以上代码中可以发现 capmem 的计算与 roundupsize() 函数有关,它在 %GOROOT%/src/runtime/msize.go 文件中,源码如下:

// 位于 %GOROOT%/src/runtime/msize.go 文件中

// 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 的大小为 32 << 10,也就是 32768
_MaxSmallSize = 32768%GOROOT/src/runtime/sizeclasses.go 文件中定义】
MaxSmallSize = 32 << 10%GOROOT/src/runtime/mksizeclasses.go 文件中定义】

roundupsize() 函数的作用是根据申请空间大小,在考虑内存对齐后返回实际分配空间大小。如果申请空间小于 _MaxSmallSize (32768),它利用 smallSizeDivsmallSizeMaxlargeSizeDiv 计算索引来从 size_to_classclass_to_size 中查值返回。

2.3 原问题 (1.1) 的实际扩容流程

以遇到的第一个问题为例,现在我们已经知道 newcap = 5;切片元素是 int 类型,所以 et.size 为 8 个字节;我的电脑是 64 位操作系统, sys.ptrSize 也为 8 个字节。所以首先在 growslice() 中的 switch 语句中会经过 case et.size == sys.PtrSize 分支,那么 capmem = roundupsize(uintptr(newcap) * sys.PtrSize),即 capmem = roundupsize(5 * 8),5 * 8 = 40 小于 _MaxSmallSize-8,因此会执行 return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]),(size+smallSizeDiv-1)/smallSizeDiv] = (40+8-1)/8 = 5,接着查下列数组可知 size_to_class8[5] = 5,class_to_size[5] = 48,最终 return 48,即 capmem = 48,而每个 int 类型占 8 个字节,所以最终的 newcap = 48 / 8 = 6,成功解释第一个例子最后一行的运行结果。

//在 %GOROOT%/src/runtime/sizeclassed.go 文件中定义
const (
	_MaxSmallSize   = 32768
	smallSizeDiv    = 8
	smallSizeMax    = 1024
	largeSizeDiv    = 128
	_NumSizeClasses = 68
	_PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{
    
    0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{
    
    0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}

var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{
    
    32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 
......
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}

三、结论总结

3.1 新问题 (1.2) 的实际扩容流程

3.1.1 向长度和容量均为 100 的切片中添加 1 个元素,最终的长度是 101,但是容量为什么是 224?

现在我们已经知道所需容量 cap = 101,原容量的 2 倍容量 doublecap = 200,cap < doublecap,那么新申请的 newcap = 200;切片元素是 int 类型,所以 et.size 为 8 个字节;我的电脑是 64 位操作系统, sys.ptrSize 也为 8 个字节。所以首先在 growslice() 中的 switch 语句中会经过 case et.size == sys.PtrSize 分支,那么 capmem = roundupsize(uintptr(newcap) * sys.PtrSize),即 capmem = roundupsize(200 * 8),200 * 8 = 1600 小于 _MaxSmallSize-8,因此会执行 return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]),(size+smallSizeDiv-1)/smallSizeDiv] = (1600+8-1)/8 = 200,接着查数组可知 size_to_class8[200] = 38,class_to_size[38] = 1792,最终 return 1792,即 capmem = 1792,而每个 int 类型占 8 个字节,所以最终的 newcap = 1792 / 8 = 224,成功解释了运行结果。

3.1.2 向长度和容量均为 10000 的切片中添加 1 个元素,最终的长度是 10001,但是容量为什么是 13312?

现在我们已经知道所需容量 cap = 10001,原容量的 2 倍容量 doublecap = 20000,cap < doublecap,那么新申请的 newcap = 20000;切片元素是 int 类型,所以 et.size 为 8 个字节;我的电脑是 64 位操作系统, sys.ptrSize 也为 8 个字节。所以首先在 growslice() 中的 switch 语句中会经过 case et.size == sys.PtrSize 分支,那么 capmem = roundupsize(uintptr(newcap) * sys.PtrSize),即 capmem = roundupsize(20000 * 8),20000 * 8 = 160000 大于 _MaxSmallSize-8,因此会执行 return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]),(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]] = (160000-1024+128-1)/128 = 1242,接着查数组可知 size_to_class128[1242] = 168,class_to_size[168] = 106496,最终 return 106496,即 capmem = 106496,而每个 int 类型占 8 个字节,所以最终的 newcap = 106496 / 8 = 13312,成功解释了运行结果。

3.2 便于理解的扩容流程

上述流程是程序执行的实际流程,中间涉及计算、查表操作,人为操作比较麻烦,我们也可以通过以下的理解方式快速得到最终结果:以遇到的第一个问题为例,现在我们已经知道所需容量 cap = 5,原容量的 2 倍容量 doublecap = 4,cap < doublecap,那么新申请的 newcap = 5;切片元素是 int 类型,所以 et.size 为 8 个字节;我的电脑是 64 位操作系统, sys.ptrSize 也为 8 个字节。所以首先在 growslice() 中的 switch 语句中会经过 case et.size == sys.PtrSize 分支,那么 capmem = roundupsize(uintptr(newcap) * sys.PtrSize),即 capmem = roundupsize(5 * 8),即传给 roundupsize() 的参数是 40,在 %GOROOT%/src/runtime/sizeclasses.go 文件中有以下代码,我们只关注第三列 bytes/span 即可,可以将其理解为一个个区间,例如 (8,16](48,64],如果传入的参数位于某个区间,则会返回该区间的上限。当我们给 roundupsize() 传入参数 40,40 位于 (32,48] 区间内,最终会返回 48,即 capmem = roundupsize(5 * 8) = 48,而每个 int 类型占 8 个字节,所以最终的 newcap = 48 / 8 = 6,快速且成功地解释了第一个例子最后一行的运行结果。

3.1.1 和 3.1.2 中的问题也可以借助这个方法理解,下面我再叙述一下 3.1.1 的理解过程,3.1.2 就请各位读者自行推导了,有问题可以在评论区留言~。

对于 3.1.1 而言,现在我们已经知道所需容量 cap = 101,原容量的 2 倍容量 doublecap = 200,cap < doublecap,那么新申请的 newcap = 200;切片元素是 int 类型,所以 et.size 为 8 个字节;我的电脑是 64 位操作系统, sys.ptrSize 也为 8 个字节。所以首先在 growslice() 中的 switch 语句中会经过 case et.size == sys.PtrSize 分支,那么 capmem = roundupsize(uintptr(newcap) * sys.PtrSize),即 capmem = roundupsize(200 * 8),即传给 roundupsize() 的参数是 1600,1600 位于 (1536,1792] 区间内,最终会返回 1792,即 capmem = 1792,而每个 int 类型占 8 个字节,所以最终的 newcap = 1792 / 8 = 224,快速且成功地解释了运行结果。

// 位于 %GOROOT%/src/runtime/sizeclasses.go 文件中

// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          8
//     4         32        8192      256           0     21.88%         32
//     5         48        8192      170          32     31.52%         16
//     6         64        8192      128           0     23.44%         64
//     7         80        8192      102          32     19.07%         16
//     8         96        8192       85          32     15.95%         32
//     9        112        8192       73          16     13.56%         16
//    10        128        8192       64           0     11.72%        128
//    11        144        8192       56         128     11.82%         16
//    12        160        8192       51          32      9.73%         32
//    13        176        8192       46          96      9.59%         16
//    14        192        8192       42         128      9.25%         64
//    15        208        8192       39          80      8.12%         16
//    16        224        8192       36         128      8.15%         32
//    17        240        8192       34          32      6.62%         16
//    18        256        8192       32           0      5.86%        256
//    19        288        8192       28         128     12.16%         32
//    20        320        8192       25         192     11.80%         64
//    21        352        8192       23          96      9.88%         32
//    22        384        8192       21         128      9.51%        128
//    23        416        8192       19         288     10.71%         32
//    24        448        8192       18         128      8.37%         64
//    25        480        8192       17          32      6.82%         32
//    26        512        8192       16           0      6.05%        512
//    27        576        8192       14         128     12.33%         64
//    28        640        8192       12         512     15.48%        128
//    29        704        8192       11         448     13.93%         64
//    30        768        8192       10         512     13.94%        256
//    31        896        8192        9         128     15.52%        128
//    32       1024        8192        8           0     12.40%       1024
//    33       1152        8192        7         128     12.41%        128
//    34       1280        8192        6         512     15.55%        256
//    35       1408       16384       11         896     14.00%        128
//    36       1536        8192        5         512     14.00%        512
//    37       1792       16384        9         256     15.57%        256
//    38       2048        8192        4           0     12.45%       2048
//    39       2304       16384        7         256     12.46%        256
//    40       2688        8192        3         128     15.59%        128
//    41       3072       24576        8           0     12.47%       1024
//    42       3200       16384        5         384      6.22%        128
//    43       3456       24576        7         384      8.83%        128
//    44       4096        8192        2           0     15.60%       4096
//    45       4864       24576        5         256     16.65%        256
//    46       5376       16384        3         256     10.92%        256
//    47       6144       24576        4           0     12.48%       2048
//    48       6528       32768        5         128      6.23%        128
//    49       6784       40960        6         256      4.36%        128
//    50       6912       49152        7         768      3.37%        256
//    51       8192        8192        1           0     15.61%       8192
//    52       9472       57344        6         512     14.28%        256
//    53       9728       49152        5         512      3.64%        512
//    54      10240       40960        4           0      4.99%       2048
//    55      10880       32768        3         128      6.24%        128
//    56      12288       24576        2           0     11.45%       4096
//    57      13568       40960        3         256      9.99%        256
//    58      14336       57344        4           0      5.35%       2048
//    59      16384       16384        1           0     12.49%       8192
//    60      18432       73728        4           0     11.11%       2048
//    61      19072       57344        3         128      3.57%        128
//    62      20480       40960        2           0      6.87%       4096
//    63      21760       65536        3         256      6.25%        256
//    64      24576       24576        1           0     11.45%       8192
//    65      27264       81920        3         128     10.00%        128
//    66      28672       57344        2           0      4.91%       4096
//    67      32768       32768        1           0     12.50%       8192

四、参考文章

  1. Go 语言之旅
  2. Go 语言切片详解,初始化、扩容、限容、底层
  3. Golang 切片(slice)扩容机制源码剖析
  4. Go 的 slice 扩容不是全部都按照 1.25 扩容的,还有内存对齐的概念,别再被忽悠了
  5. Go 切片的用法和本质

猜你喜欢

转载自blog.csdn.net/Alan_Walker688/article/details/127765704