go slice 扩容实现原理源码解析

目录
  • 正文
  • 扩容的示例
  • 实际扩容倍数
  • growslice 实现
    • growslice 实现步骤
    • growslice 源码剖析
  • 总结

正文

基于 Go 1.19。

go 的切片我们都知道可以自动地进行扩容,具体来说就是在切片的容量容纳不下新的元素的时候, 底层会帮我们为切片的底层数组分配更大的内存空间,然后把旧的切片的底层数组指针指向新的内存中:

目前网上一些关于扩容倍数的文章都是基于相对旧版本的 Go 的,新版本中,现在切片扩容的时候并不是那种准确的小于多少容量的时候就 2 倍扩容, 大于多少容量的时候就 1.25 倍扩容,其实这个数值多少不是非常关键的,我们只需要知道的是: 在容量较小的时候,扩容的因子更大,容量大的时候,扩容的因子相对来说比较小

扩容的示例

我们先通过一个简单的示例来感受一下切片扩容是什么时候发生的:

var slice = []int{1, 2, 3}
fmt.Println(slice, len(slice), cap(slice))
slice = append(slice, 4)
fmt.Println(slice, len(slice), cap(slice))

在这个例子中,slice 切片初始化的时候,长度和容量都是 3(容量不指定的时候默认等于长度)。 因此切片已经容纳不下新的元素了,在我们往 slice 中追加一个新的元素的时候, 我们发现,slice 的长度和容量都变了, 长度增加了 1,而容量变成了原来的 2 倍。

在 1.18 版本以后,旧的切片容量小于 256 的时候,会进行 2 倍扩容。

实际扩容倍数

其实最新的扩容规则在 1.18 版本中就已经发生改变了,具体可以参考一下这个 commitruntime: make slice growth formula a bit smoother

大概意思是:

在之前的版本中:对于 <1024 个元素,增加 2 倍,对于 >=1024 个元素,则增加 1.25 倍。 而现在,使用更平滑的增长因子公式。 在 256 个元素后开始降低增长因子,但要缓慢。

它还给了个表格,写明了不同容量下的增长因子:

starting cap growth factor
256 2.0
512 1.63
1024 1.44
2048 1.35
4096 1.30

从这个表格中,我们可以看到,新版本的切片库容,并不是在容量小于 1024 的时候严格按照 2 倍扩容,大于 1024 的时候也不是严格地按照 1.25 倍来扩容。

growslice 实现

在 go 中,切片扩容的实现是 growslice 函数,位于 runtime/slice.go 中。

growslice 有如下参数:

  • oldPtr: 旧的切片的底层数组指针。
  • newLen: 新的切片的长度(= oldLen + num)。
  • oldCap: 旧的切片的容量。
  • num: 添加的元素数。
  • et: 切片的元素类型(也即 element type)。

返回一个新的切片,这个返回的切片中,底层数组指针指向新分配的内存空间,长度等于 oldLen + num,容量就是底层数组的大小。

growslice 实现步骤

  • 一些特殊情况判断:如 et.size == 0,切片元素不需要占用空间的情况下,直接返回。
  • 根据 newLen 计算新的容量,保证新的底层数组至少可以容纳 newLen 个元素。
  • 计算所需要分配的新的容量所需的内存大小。
  • 分配新的切片底层数组所需要的内存。
  • 将旧切片上的底层数组的数据复制到新的底层数组中。

注意:这个函数只是实现扩容,新增的元素没有在这个函数往切片中追加。

growslice 源码剖析

说明:

  • 整数有可能会溢出,所以代码里面会判断 newLen < 0
  • 如果切片的元素是空结构体或者空数组,那么 et.size == 0
  • 在计算新切片的容量的时候,会根据切片的元素类型大小来做一些优化。
  • 新切片容量所占用的内存大小为 capmem
  • 新切片所需要的内存分配完成后,会将旧切片的数据复制到新切片中。
  • 最后返回指向新的底层数组的切片,其长度为 newLen,容量为 newcap
// growtslice 为切片分配新的存储空间。
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
   // oldLen 为旧的切片底层数组的长度
   oldLen := newLen - num
   // 分配的新的长度不能小于 0(整数溢出的时候会是负数)
   if newLen < 0 {
      panic(errorString("growslice: len out of range"))
   }
   // 如果结构或数组类型不包含大小大于零的字段(或元素),则其大小为零。
   //(空数组、空结构体,type b [0]int、type zero struct{})
   // 两个不同的零大小变量在内存中可能具有相同的地址。
   if et.size == 0 {
      // append 不应创建具有 nil 指针但长度非零的切片。
      // 在这种情况下,我们假设 append 不需要保留 oldPtr。
      return slice{unsafe.Pointer(&zerobase), newLen, newLen}
   }
   // newcap 是新切片底层数组的容量
   newcap := oldCap
   // 两倍容量
   doublecap := newcap + newcap
   if newLen > doublecap {
      // 如果追加元素之后,新的切片长度比旧切片 2 倍容量还大,
      // 则将新的切片的容量设置为跟长度一样
      newcap = newLen
   } else {
      const threshold = 256
      if oldCap < threshold {
         // 旧的切片容量小于 256 的时候,
         // 进行两倍扩容。
         newcap = doublecap
      } else {
         // oldCap >= 256
         // 检查 0<newcap 以检测溢出并防止无限循环。
         for 0 < newcap && newcap < newLen {
            // 从小切片的增长 2 倍过渡到大切片的增长 1.25 倍。
            newcap += (newcap + 3*threshold) / 4
         }
         // 当 newcap 计算溢出时,将 newcap 设置为请求的上限。
         if newcap <= 0 {
            newcap = newLen
         }
      }
   }
   // 计算实际所需要的内存大小
   // 是否溢出
   var overflow bool
   // lenmem 表示旧的切片长度所需要的内存大小
   //(lenmem 就是将旧切片数据复制到新切片的时候指定需要复制的内存大小)
   // newlenmem 表示新的切片长度所需要的内存大小
   // capmem 表示新的切片容量所需要的内存大小
   var lenmem, newlenmem, capmem uintptr
   // 根据 et.size 做一些计算上的优化:
   // 对于 1,我们不需要任何除法/乘法。
   // 对于 goarch.PtrSize,编译器会将除法/乘法优化为移位一个常数。
   // 对于 2 的幂,使用可变移位。
   switch {
   case et.size == 1: // 比如 []byte,所需内存大小 = size
      lenmem = uintptr(oldLen)
      newlenmem = uintptr(newLen)
      capmem = roundupsize(uintptr(newcap))
      overflow = uintptr(newcap) > maxAlloc
      newcap = int(capmem)
   case et.size == goarch.PtrSize: // 比如 []*int,所需内存大小 = size * ptrSize
      lenmem = uintptr(oldLen) * goarch.PtrSize
      newlenmem = uintptr(newLen) * goarch.PtrSize
      capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
      overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
      newcap = int(capmem / goarch.PtrSize)
   case isPowerOfTwo(et.size): // 比如 []int64,所需内存大小 = size << shift,也就是 size * 2^shift(2^shift 是 et.size)
      var shift uintptr
      if goarch.PtrSize == 8 {
         // Mask shift for better code generation.
         shift = uintptr(sys.TrailingZeros64(uint64(et.size))) & 63
      } else {
         shift = uintptr(sys.TrailingZeros32(uint32(et.size))) & 31
      }
      lenmem = uintptr(oldLen) << shift
      newlenmem = uintptr(newLen) << shift
      capmem = roundupsize(uintptr(newcap) << shift)
      overflow = uintptr(newcap) > (maxAlloc >> shift)
      newcap = int(capmem >> shift)
      capmem = uintptr(newcap) << shift
   default: // 没得优化,直接使用乘法了
      lenmem = uintptr(oldLen) * et.size
      newlenmem = uintptr(newLen) * et.size
      capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
      capmem = roundupsize(capmem)
      newcap = int(capmem / et.size)
      capmem = uintptr(newcap) * et.size
   }
   // 检查是否溢出,以及是否超过最大可分配内存
   if overflow || capmem > maxAlloc {
      panic(errorString("growslice: len out of range"))
   }
   // 分配实际所需要的内存
   var p unsafe.Pointer
   if et.ptrdata == 0 { // 不包含指针
      // 分配 capmem 大小的内存,不清零
      p = mallocgc(capmem, nil, false)
      // 这里只清空从 add(p, newlenmem) 开始大小为 capmem-newlenmem 的内存,
      // 也就是前面的 newlenmem 长度不清空。
      // 因为最后的 capmem-newlenmem 这块内存,实际上是额外分配的容量。
      // 前面的那部分会被旧切片的数据以及新追加的数据覆盖。
      memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
   } else {
      // 分配 capmem 大小的内存,需要进行清零
      p = mallocgc(capmem, et, true)
      if lenmem > 0 && writeBarrier.enabled {
         // Only shade the pointers in oldPtr since we know the destination slice p
         // only contains nil pointers because it has been cleared during alloc.
         bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.size+et.ptrdata)
      }
   }
   // 旧切片数据复制到新切片中,复制的内容大小为 lenmem
   //(从 oldPtr 复制到 p)
   memmove(p, oldPtr, lenmem)
   return slice{p, newLen, newcap}
}

总结

go 的切片在容量较小的情况下,确实会进行 2 倍扩容,但是随着容量的增长,扩容的增长因子会逐渐降低。 新版本的 growslice 实现中,只有容量小于 256 的时候才会进行 2 倍扩容, 然后随着容量的增长,扩容的因子会逐渐降低(但并不是直接降到 1.25,而是一个相对缓慢的下降)。

以上就是go slice 扩容实现原理源码解析的详细内容,更多关于go slice 扩容原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • go数据结构和算法BitMap原理及实现示例

    目录 1. BitMap介绍 如何判断数字在bit数组的位置 设置数据到bit数组 从bit数组中清除数据 数字是否在bit数组中 2. Go语言位运算 左移 右移 使用&^和位移运算来给某一位置0 3. BitMap的Go语言实现 定义 创建BitMap结构 将数据添加到BitMap 从BitMap中删除数据 判断BitMap中是否存在指定的数据 1. BitMap介绍 BitMap可以理解为通过一个bit数组来存储特定数据的一种数据结构.BitMap常用于对大量整形数据做去重和查询.在这类查

  • Go压缩位图库roaring安装使用详解

    目录 简介 安装 使用 基本操作 迭代 并行操作 写入与读取 64 位版本 存储格式 概览 Cookie Header Descriptive Header Offset Header Container array bitmap/bitset run 手撸解析代码 总结 简介 集合是软件中的基本抽象.实现集合的方法有很多,例如 hash set.tree等.要实现一个整数集合,位图(bitmap,也称为 bitset 位集合,bitvector 位向量)是个不错的方法.使用 n 个位(bit)

  • go sync Once实现原理示例解析

    目录 正文 Once 的实现 使用示例 Once 的一些工作机制 Once 详解 hotpath atomic.LoadUint32 atomic.StoreUint32 Mutex 总结 正文 在很多情况下,我们可能需要控制某一段代码只执行一次,比如做某些初始化操作,如初始化数据库连接等. 对于这种场景,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次. 当然我们也是可以自己通过 Mutex 实现 sync.Once 的功能,但是相比来说繁琐了那么一点, 因为我们不仅

  • go reflect要不要传指针原理详解

    目录 正文 什么时候传递指针? 1. 通过传递指针修改变量的值 传值无法修改变量本身 传指针可以修改变量 2. 通过传递指针修改结构体的字段 3. 结构体:获取指针接收值方法 4. 变量本身包含指向数据的指针 通过值反射对象修改 chan.map 和 slice slice 反射对象扩容的影响 slice 容量够的话是不是就可以正常追加元素了? map 也不能通过值反射对象来修改其元素. chan 没有追加 结构体字段包含指针的情况 5. interface 类型处理 interface 底层类

  • RoaringBitmap原理及在Go中的使用详解

    目录 引言 1 什么是 RoaringBitmap 2 数据结构 3 三种 Container 3.1 ArrayContainer 3.2 BitmapContainer 3.3 RunContainer 4 Go 使用 RoaringBitmap 4.1 并集运算 4.2 交集运算 4.3 差集运算 4.4 异或运算 5 总结 引言 今天我们聊聊 RoaringBitmap(咆哮位图).在海量数据背景下,我们通常需要快速对数据计算.中间存储的需求.一系列专门为大数据准备的数据结构应运而生,常

  • go slice 扩容实现原理源码解析

    目录 正文 扩容的示例 实际扩容倍数 growslice 实现 growslice 实现步骤 growslice 源码剖析 总结 正文 基于 Go 1.19. go 的切片我们都知道可以自动地进行扩容,具体来说就是在切片的容量容纳不下新的元素的时候, 底层会帮我们为切片的底层数组分配更大的内存空间,然后把旧的切片的底层数组指针指向新的内存中: 目前网上一些关于扩容倍数的文章都是基于相对旧版本的 Go 的,新版本中,现在切片扩容的时候并不是那种准确的小于多少容量的时候就 2 倍扩容, 大于多少容量

  • Spring的Model 和 Map的原理源码解析

    Model 和 Map 为什么在Model和Map中放值传入后会出现在request的上面. 9.1.源码解析 准备测试代码 @GetMapping("/goto") public String go(HttpServletRequest request, Map<String,Object> map, Model model){ request.setAttribute("msg","传过来...."); map.put("

  • MyBatis框架底层的执行原理源码解析

    目录 1.前言 2.案例项目源码 3.MyBatis源码解析底层执行原理 3.1 读取mybatis配置文件创建出SqlSeesionFactory对象 3.2 通过SqlSeesionFactory对象进而创建出SqlSession对象 3.3 通过SqlSession的getMapper获取到接口代理对象 3.4 通过mapper接口的代理对象执行CRUD 1.前言 MyBatis框架大家肯定都用过的,废话我就不再多说了,这篇文章就给大家分享一下有关MyBatis框架底层的执行原理吧(Deb

  • React Hydrate原理源码解析

    目录 引言 Demo ReactDOM.render ReactDOM.hydrate hydrate 过程 事件绑定 hydrate 源码剖析 beginWork HostRoot Fiber HostComponent HostText Fiber tryToClaimNextHydratableInstance completeUnitOfWork popHydrationState prepareToHydrateHostInstance prepareToHydrateHostText

  • axios拦截器工作方式及原理源码解析

    目录 axios 拦截器的配置方式 use() 方法的定义 拦截器如何执行 拦截器回调方法的添加顺序 同步执行请求拦截器(顺序执行) 异步执行请求拦截器(同时执行) Q&A 拦截器是如何工作的 拦截器的执行顺序 同步&异步 axios 拦截器的配置方式 本文所用 axios 版本号为:1.3.2. axios 中有两种拦截器: axios.interceptors.request.use(onFulfilled, onRejected, options):配置请求拦截器. onFulfil

  • async-validator实现原理源码解析

    目录 async-validator 介绍 async-validator 基本使用 async-validator 源码分析 async-validator 源码-构造函数 async-validator 源码-validate方法 async-validator 源码-register方法 总结 最后 async-validator 介绍 async-validator是异步的验证数据是否合法有效的工具, 内置了不同数据类型的常见验证规则. 在需要对数据进行验证的场景中,都可以考虑使用asy

  • python装饰器原理源码示例分析

    目录 前言 一.什么是装饰器 二.为什么要用装饰器 三.简单的装饰器 四.装饰器的语法糖 五.装饰器传参 六.带参数的装饰器 七.类装饰器 八.带参数的类装饰器 九.装饰器的顺序 前言 最近有人问我装饰器是什么,我就跟他说,其实就是装饰器就是类似于女孩子的发卡.你喜欢的一个女孩子,她可以有很多个发卡,而当她戴上不同的发卡,她的头顶上就是装饰了不同的发卡.但是你喜欢的女孩子还是你喜欢的女孩子.如果还觉得不理解的话,装饰器就是咱们的手机壳,你尽管套上了手机壳,但并不影响你的手机功能,可你的手机还是该

  • Vue解读之响应式原理源码剖析

    目录 初始化 initState() initProps() initData() observe() Observer defineReactive() 依赖收集 Dep Watcher 依赖收集过程 移除订阅 派发更新 notify() update() queueWatcher() flushSchedulerQueue() updated() defineProperty 缺陷及处理 Vue.set() 重写数组方法 总结 先看张图,了解一下大体流程和要做的事 初始化 在 new Vue

  • Vue watch原理源码层深入讲解

    目录 添加依赖 触发依赖 总结 由于我在从源码看vue(v2.7.10)的computed的实现原理中详细的讲解过computed的实现,本篇跟computed的原理类似.我就带大家简单分析一下. 添加依赖 代码如下: <template> <div> {{a}} <button @click="addModule">新增</button> </div> </template> <script> exp

  • spring-session简介及实现原理源码分析

    一:spring-session介绍 1.简介 session一直都是我们做集群时需要解决的一个难题,过去我们可以从serlvet容器上解决,比如开源servlet容器-tomcat提供的tomcat-redis-session-manager.memcached-session-manager. 或者通过nginx之类的负载均衡做ip_hash,路由到特定的服务器上.. 但是这两种办法都存在弊端. spring-session是spring旗下的一个项目,把servlet容器实现的httpSe

随机推荐