Go源码分析之预分配slice内存
作者:Goland猫
切片扩容
在 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
在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 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源码分析之预分配slice内存的文章就介绍到这了,更多相关Go预分配slice内存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!