Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go预分配slice内存

Go源码分析之预分配slice内存

作者:Goland猫

这篇文章主要从Go语言源码带大家分析一下预分配slice内存的相关知识,文中的示例代码简洁易懂,对我们深入了解go有一定的帮助,需要的可以学习一下

切片扩容

在 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 < cap { //如果新容量 > 0  并且 原容量 小于 所需容量
				// 从小片的增长2x过渡到大片的增长1.25x。这个公式给出了两者之间的平滑过渡。
				newcap += (newcap + 3*threshold) / 4
				//新容量是 = 1.25 原容量 + 3/4 阈值 (192)
			}
			//当newcap计算溢出时,将newcap设置为请求的上限。
			if newcap <= 0 { // 如果发生了溢出,将新容量设置为请求的容量大小
				newcap = cap
			}
		}
	}
}

函数判断如果所需容量 cap 大于两倍扩容的容量 doublecap,说明 cap 的需求已经超过了两倍扩容的范围,所以将 newcap 直接设为 cap

否则,如果原容量 old.cap 小于一个阈值 threshold(这里设为256),则将 newcap 设置为原容量的两倍 doublecap

如果既不满足上述条件,则进入一个循环,只要 newcap 大于0且小于所需容量 cap,就会进入循环。在每次循环迭代中,newcap 的增长方式为当前 newcap 加上 3*threshold 的四分之一。这个计算方式使得容量的增长逐渐从原容量的两倍过渡到1.25倍,实现了一个平滑的过渡。

最后,在循环结束后,判断如果 newcap 仍然小于等于0(溢出情况),则将 newcap 设为所需容量 cap

如果 现有容量 小于 256 ,则新容量是原来的两倍

新容量 = 1.25 原容量 + 3/4 阈值 (192) “这个公式给出了从1.25倍增长 过渡到2 倍增长,两者之间的平滑过渡。” 在此情况下,如果发生了溢出,将新容量设置为请求的容量大小

代码测试

func main() {
	slice := make([]int, 0)
	for i := 0; i < 512; i++ {
		slice = append(slice, i)
	}
	newSlice := append(slice, 5000)
	fmt.Printf("Before Pointer = %p, len = %d, cap = %d\n", &slice, len(slice), cap(slice))
	fmt.Printf("Before Pointer = %p, len = %d, cap = %d\n", &newSlice, len(newSlice), cap(newSlice))
}

执行输出如下:

再看一个例子

下面用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。

package main  
import "fmt"  
func main() {  
    s := make([]int, 0)  
    oldCap := cap(s)  
    for i := 0; i < 2048; i++ {  
        s = append(s, i)  
        newCap := cap(s)  
        if newCap != oldCap {  
            fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)  
            oldCap = newCap  
        }  
    }  
}

运行结果(1.17 版本):

[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 = 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

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

运行结果(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

内存对齐

但是,后半部分还对 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源码分析之预分配slice内存的文章就介绍到这了,更多相关Go预分配slice内存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文