GO进阶源码分析之 【Go预分配 slice 内存

2023年 8月 15日 19.3k 0

切片扩容

在 1.18 版本前,切片扩容,在容量小于1024时,以2倍大小扩容。超过1024后,以1.25倍扩容。在扩容后切片的基础上,会根据长度和容量进行 roundupsize 。

在1.18版本后,接下来看一下源码如下:

func growslice(et *_type, old slice, cap int) slice {
	//  ......
	newcap := old.cap
	doublecap := newcap + newcap //双倍扩容(原容量的两倍)
	if cap > doublecap {         //如果所需容量大于 两倍扩容,则直接扩容到所需容量
		newcap = cap
	} else {
		const threshold = 256    //这里设置了一个 阈值 -- 256
		if old.cap < threshold { //如果旧容量 小于 256,则两倍扩容
			newcap = doublecap
		} else {
			// 检查 0 < newcap 以检测溢出并防止无限循环。
			for 0 < newcap && newcap  0  并且 原容量 小于 所需容量

				// 从小片的增长2x过渡到大片的增长1.25x。这个公式给出了两者之间的平滑过渡。
				newcap += (newcap + 3*threshold) / 4
				//新容量是 = 1.25 原容量 + 3/4 阈值 (192)
			}
			//当newcap计算溢出时,将newcap设置为请求的上限。
			if newcap    -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256 
[0 ->  255] cap = 256   |  after append 256   cap = 512 
[0 ->  511] cap = 512   |  after append 512   cap = 1024
[0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
[0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于 1024 就会将容量翻倍;
  • 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
  • 运行结果(1.18 版本):

    [0 ->   -1] cap = 0     |  after append 0     cap = 1  
    [0 ->    0] cap = 1     |  after append 1     cap = 2     
    [0 ->    1] cap = 2     |  after append 2     cap = 4     
    [0 ->    3] cap = 4     |  after append 4     cap = 8     
    [0 ->    7] cap = 8     |  after append 8     cap = 16    
    [0 ->   15] cap = 16    |  after append 16    cap = 32    
    [0 ->   31] cap = 32    |  after append 32    cap = 64    
    [0 ->   63] cap = 64    |  after append 64    cap = 128   
    [0 ->  127] cap = 128   |  after append 128   cap = 256   
    [0 ->  255] cap = 256   |  after append 256   cap = 512   
    [0 ->  511] cap = 512   |  after append 512   cap = 848   
    [0 ->  847] cap = 848   |  after append 848   cap = 1280  
    [0 -> 1279] cap = 1280  |  after append 1280  cap = 1792  
    [0 -> 1791] cap = 1792  |  after append 1792  cap = 2560
    

    在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  • 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold,直到新容量大于期望容量;
  • 内存对齐

    但是,后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 按照前半部分生成的newcap

    之后,向 Go 内存管理器申请内存,将老 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中。

    最后,向 growslice 函数调用者返回一个新的 slice,这个 slice 的长度并没有变化,而容量却增大了。

    测试代码

    import "fmt"
    
    func main() {
    	s := []int{1,2}
    	s = append(s,4,5,6)
    	fmt.Printf("len=%d, cap=%d",len(s),cap(s))
    }
    

    输出

    len=5, cap=6

    根据Go语言中切片的扩容机制,当切片容量不足以容纳额外的元素时,它会自动进行扩容。在这种情况下,切片的容量会根据需要自动增长,通常会以原来容量的2倍进行扩容。

    根据您的代码,初始切片s的容量为2,添加了3个元素后,长度变为5。实际上,在这种情况下,切片的容量不会立即扩大到8,而是继续保持为6。这是因为Go语言的切片扩容机制会优化以减少内存的浪费。扩容时,切片会选择一个合适的容量,使得容量尽可能靠近但大于所需的最小容量。

    源码分析

    func growslice(et *_type, old slice, cap int) slice {
        // ……
        newcap := old.cap
    	doublecap := newcap + newcap
    	if cap > doublecap {
    		newcap = cap
    	} else {
    		// ……
    	}
    	// ……
    	
    	capmem = roundupsize(uintptr(newcap) * ptrSize)
    	newcap = int(capmem / ptrSize)
    }
    

    capmem = roundupsize(uintptr(newcap) * ptrSize):根据 newcap 和指针的大小计算需要分配的内存大小。
    newcap = int(capmem / ptrSize):通过将内存大小除以指针大小,将新的容量 newcap 转换为整数。

    参考文章:

    • go.dev/doc/go1.18
    • go.dev/blog/slices
    • go.dev/blog/slices…
    • golang.design/go-question…
    • draveness.me/golang/docs…

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论