当前位置: 首页 > article >正文

深入理解 Go slice 扩容机制

前言

go 的切片可以自动地进行扩容,当调用append方法时,如果len == cap,即容纳不下新的元素的时, 底层会帮我们为切片的底层数组分配更大的内存空间,然后把旧的切片的底层数组指针指向新的内存中

  • 注意不是根据底层数据的容量来判断是否扩容,而是根据slice的属性cap,cap可能比数组实际容量小

分配更大的内存时预留的一定的buffer

  • 如果不预留,只考虑当前新增的元素,则每次append时都会发生数据拷贝,成本太高

于是slice扩容的重点就在于,预留多少buffer?

1.18以前

直接看源码:

源码位置:/src/runtime/slice.go growslice

func growslice(et *_type, old slice, cap int) slice {
   // ...

   newcap := old.cap
   doublecap := newcap + newcap
   if cap > doublecap {
      newcap = cap
   } else {
      if old.cap < 1024 {
         newcap = doublecap
      } else {
         // Check 0 < newcap to detect overflow
         // and prevent an infinite loop.
         for 0 < newcap && newcap < cap {
            newcap += newcap / 4
         }
         if newcap <= 0 {
            newcap = cap
         }
      }
   }
   
   // ...
   
   capmem = roundupsize(uintptr(newcap))

新容量的计算规则如下:

  • 需要的容量比2倍老容量大:使用需要的容量

    • 一般发生于append一个较大的slice时,例如 append(s, s1...)
  • 如果老容量小于1024,按照2倍扩容

  • 如果老容量大于等于1024,按照1.25倍扩容,对应源码newcap += newcap / 4

  • 如果newcap溢出了int最大值,不扩容

最后再按照内存分配块的大小,向上修正

内存分配各个size如下:{0, 8, 16, 24, 32, 48, 64, 80, 96, 112...}

例如,如果slice为int类型,有5个元素,应该占40个字节,但由于分配到size=48的内存块,会向上修正到48字节,也就是cap变为6

func main() {
   s := []int{}
   s = append(s, []int{1, 2, 3, 4, 5}...)
   fmt.Println(cap(s))  // 结果为6
}

这个扩容规则有什么问题?

  1. 不够平滑:容量小于1024时2倍扩容,大于1024突然降到1.25倍扩容

  2. 容量增长不单调,正常应该是较大的初始容量扩容后有较大的最终容量

    1. 例如以下代码:
x1 := make([]int, 897)
x2 := make([]int, 1024)
y := make([]int, 100)
println(cap(append(x1, y...))) // 2048
println(cap(append(x2, y...))) // 1280

x1的容量比x2小,都增加100个元素后x1的容量反而比x2大

1.18以后

go在这次提交中 runtime: make slice growth formula a bit smoother修改了扩容规则:

其用一个更平滑的公式来计算扩容因子,在256个元素之前还是2倍扩容因子,在256个元素后逐步减少,不同容量下的扩容因子如下:

starting capgrowth factor
2562.0
5121.63
10241.44
20481.35
40961.30

改版后代码如下:

// ...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
   newcap = cap
} else {
   const threshold = 256
   if old.cap < threshold {
      newcap = doublecap
   } else {
      for 0 < newcap && newcap < cap {
         newcap += (newcap + 3*threshold) / 4
      }
      if newcap <= 0 {
         newcap = cap
      }
   }
}
// ...

在容量256以后,预留的buffer容量为 1/4原容量,加上了 3/4 * 256

这样当old.cap为256时还是2倍扩容,随着容量增大3/4 * 256占原容量的比例逐渐降低最终收敛于0,达到平滑扩容的效果

总结

  • 1.18以前:

    • 容量小于1024时为2倍扩容,大于等于1024时为1.25倍扩容
  • 1.18以后:

    • 小于256时为2倍扩容,大于等于256时的扩容因子逐渐从2减低为1.25
  • 不管1.18前后,最终需要按照内存分配块向上修正


http://www.kler.cn/a/1245.html

相关文章:

  • Kotlin学习(一)
  • Unity + Firebase + GoogleSignIn 导入问题
  • .NET体系架构
  • 有收到腾讯委托律师事务所向AppStore投诉带有【水印相机】主标题名称App的开发者吗
  • 麦田物语学习笔记:背包物品选择高亮显示和动画
  • 词作词汇积累:错付、大而无当、语焉不详、愈演愈烈
  • Redis基础篇
  • Spring 事务(编程式事务、声明式事务@Transactional、事务隔离级别、事务传播机制)
  • Spring事务和事务传播机制
  • 插件化架构设计(2):插件化从设计到实践该考量的问题汇总
  • 菱形继承和C++相关问题
  • React 用一个简单案例体验一遍 React-dom React-router React-redux 全家桶
  • Springboot集成Swagger
  • 公司测试员用例写得乱七八糟,测试总监制定了这份《测试用例编写规范》
  • 【高阶数据结构】红黑树
  • css属性学习
  • Java基础之常见运算符
  • 77.qt qml-QianWindow-V1版本界面讲解
  • @JsonFormat与@DateTimeFormat
  • go语言如何使用new构造Map
  • 【技术方案】常见库存设计方案-各种方案对比总有一个适合你
  • 百度的文心一言 ,没有想像中那么差
  • 西安石油大学C语言期末重点知识点总结
  • 链表 算法
  • 盖子的c++小课堂——第十五讲:基础排序
  • Linux: 以太网 PHY 驱动简析