go slice 扩容实现

基于 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 版本中就已经发生改变了,具体可以参考一下这个 commit
runtime: make slice growth formula a bit smoother。

大概意思是:

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

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

starting capgrowth factor
2562.0
5121.63
10241.44
20481.35
40961.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 实现步骤

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

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

growslice 源码剖析

说明:

  1. 整数有可能会溢出,所以代码里面会判断 newLen < 0
  2. 如果切片的元素是空结构体或者空数组,那么 et.size == 0
  3. 在计算新切片的容量的时候,会根据切片的元素类型大小来做一些优化。
  4. 新切片容量所占用的内存大小为 capmem
  5. 新切片所需要的内存分配完成后,会将旧切片的数据复制到新切片中。
  6. 最后返回指向新的底层数组的切片,其长度为 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 + newcapif newLen > doublecap {// 如果追加元素之后,新的切片长度比旧切片 2 倍容量还大,// 则将新的切片的容量设置为跟长度一样newcap = newLen} else {const threshold = 256if 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,所需内存大小 = sizelenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == goarch.PtrSize: // 比如 []*int,所需内存大小 = size * ptrSizelenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap) * goarch.PtrSize)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.size): // 比如 []int64,所需内存大小 = size << shift,也就是 size * 2^shift(2^shift 是 et.size)var shift uintptrif 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) << shiftnewlenmem = uintptr(newLen) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault: // 没得优化,直接使用乘法了lenmem = uintptr(oldLen) * et.sizenewlenmem = uintptr(newLen) * et.sizecapmem, 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.Pointerif 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,而是一个相对缓慢的下降)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/649914.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis源码之:clion搭建cluster环境

cluster集群通常每个node节点都是一主N从的模式&#xff0c;此处为简化环境搭建&#xff0c;所有node节点均只有一个主节点。 在clion环境中&#xff0c;为方便debug&#xff0c;需要通过配置多个cmake application实现redis-server、redis-cli等源码debug模式启动。 一、配置…

费曼学习法 - 理工科的学习利器

费曼学习法是以物理学家理查德费曼&#xff08;Richard Feynman&#xff09;命名的一种高效的学习方法。它旨在帮助你深入理解复杂概念&#xff0c;并能够用简单的语言解释它们。费曼学习法是一个学习框架&#xff0c;能够帮你对给定主题进行深入理解&#xff0c;包含以下4个简…

HTML-表单

表单 概念&#xff1a;一个包含交互的区域&#xff0c;用于收集用户提供的数据。 1.基本结构 示例代码&#xff1a; <form action"https://www.baidu.com/s" target"_blank" method"get"><input type"text" name"wd&q…

完成NAT实验

实验要求&#xff1a; 步骤一&#xff1a;配置vlan vlan b 2 3 interface GigabitEthernet 0/0/2 port link-type access port default vlan 2 interface GigabitEthernet 0/0/3 port link-type access port default vlan 3 interface GigabitEthernet 0/0/1 port link-type…

【PyTorch】使用PyTorch创建卷积神经网络并在CIFAR-10数据集上进行分类

前言 在深度学习的世界中&#xff0c;图像分类任务是一个经典的问题&#xff0c;它涉及到识别给定图像中的对象类别。CIFAR-10数据集是一个常用的基准数据集&#xff0c;包含了10个类别的60000张32x32彩色图像。在本博客中&#xff0c;我们将探讨如何使用PyTorch框架创建一个简…

C#,打印漂亮杨辉三角形(帕斯卡三角形)的源代码

杨辉 Blaise Pascal 这是某些程序员看完会哭的代码。 杨辉三角形&#xff08;Yanghui Triangle&#xff09;&#xff0c;是一种序列数值的三角形几何排列&#xff0c;最早出现于南宋数学家杨辉1261年所著的《详解九章算法》一书。 欧洲学者&#xff0c;最先由帕斯卡&#x…

Windows打开IE浏览器命令最简单的方法

问题场景&#xff1a; 许多插件或特定版本的系统需要使用ie浏览器来访问&#xff0c;window默认的ie浏览器是被禁用的如何快速打开ie浏览器解决问题 目录 问题场景&#xff1a; 测试环境&#xff1a; 检查环境是否支持&#xff1a; 问题解决&#xff1a; 方法一 方法二 方法…

03 SB实战 -微头条之首页门户模块(跳转某页面自动展示所有信息+根据hid查询文章全文并用乐观锁修改阅读量)

1.1 自动展示所有信息 需求描述: 进入新闻首页portal/findAllType, 自动返回所有栏目名称和id 接口描述 url地址&#xff1a;portal/findAllTypes 请求方式&#xff1a;get 请求参数&#xff1a;无 响应数据&#xff1a; 成功 {"code":"200","mes…

hex 尽然可以 设置透明度,透明度参数对比图 已解决

还不知道CSS Color Module Level 4标准早在2014年就推出8位hex和4位hex来支持设置alpha值&#xff0c;以实现hex和rgba的互转。这个办法可比6位HEX转RGBA简洁多了&#xff0c;先来简单解释一下&#xff1a; 8位hex是在6位hex基础上加后两位来表示alpha值&#xff0c;00表示完全…

Hadoop-MapReduce-MRAppMaster启动篇

一、源码下载 下面是hadoop官方源码下载地址&#xff0c;我下载的是hadoop-3.2.4&#xff0c;那就一起来看下吧 Index of /dist/hadoop/core 二、上下文 在上一篇<Hadoop-MapReduce-源码跟读-客户端篇>中已经将到&#xff1a;作业提交到ResourceManager&#xff0c;那…

数据结构——树的合集

目录 文章目录 前言 一.树的表达方式 1.树的概念 2.树的结点 3.树的存储结构 01.双亲表示法 顺序表示形式 优缺点说明 02.孩子表示法 03.孩子兄弟表示法 04.非类存储代码演示 二.二叉树 1.树的特点 2.二叉树 01.定义 02.二叉树的性质 03.满二叉树 04.完全二叉树…

uniapp封装公共的方法或者数据请求方法

仅供自己参考&#xff0c;不是每个页面都用到这个方法&#xff0c;所以我直接在用到的页面引用该公用方法&#xff1a; 1、新建一个util.js文件 export const address function(options){return new Promise((resolve,reject)>{uni.request({url:"https://x.cxniu.…

Istio-gateway

一. gateway 在 Kubernetes 环境中&#xff0c;Kubernetes Ingress用于配置需要在集群外部公开的服务。但是在 Istio 服务网格中&#xff0c;更好的方法是使用新的配置模型&#xff0c;即 Istio Gateway&#xff0c;Gateway 允许将 Istio 流量管理的功能应用于进入集群的流量&…

Android P 背光机制流程分析

在android 9.0中&#xff0c;相比android 8.1而言&#xff0c;背光部分逻辑有较大的调整&#xff0c;这里就对android P背光机制进行完整的分析。 1.手动调节亮度 1.1.在SystemUI、Settings中手动调节 在界面(SystemUI)和Settings中拖动进度条调节亮度时&#xff0c;调节入口…

Excel 2019 for Mac/Win:商务数据分析与处理的终极工具

在当今快节奏的商业环境中&#xff0c;数据分析已经成为一项至关重要的技能。从市场趋势预测到财务报告&#xff0c;再到项目管理&#xff0c;数据无处不在。而作为数据分析的基石&#xff0c;Microsoft Excel 2019 for Mac/Win正是一个强大的工具&#xff0c;帮助用户高效地处…

face_recognition和图像处理中left、top、right、bottom解释

face_recognition.face_locations 介绍 加载图像文件后直接调用face_recognition.face_locations(image)&#xff0c;能定位所有图像中识别出的人脸位置信息&#xff0c;返回值是列表形式&#xff0c;列表中每一行是一张人脸的位置信息&#xff0c;包括[top, right, bottom, l…

微服务-微服务Alibaba-Nacos注册中心实现

1. 系统架构的演变 俗话说&#xff0c; 没有最好的架构&#xff0c;只有最合适的架构。 微服务架构也是随着信息产业的发展而出现的最有普 遍适用性的一套架构模式。通常来说&#xff0c;我们认为架构发展历史经历了这样一个过程&#xff1a;单体架构——> 垂直架构 ——&g…

go实现生成html文件和html文件浏览服务

文章目录 本文章是为了解决 使用Jenkins执行TestNgSeleniumJsoup自动化测试和生成ExtentReport测试报告生成的测试报告&#xff0c;只能在jenkins里面访问&#xff0c;为了方便项目组内所有人员都能查看测试报&#xff0c;可以在jenkins构建时&#xff0c;把测试报告的html推送…

Leetcode—114. 二叉树展开为链表【中等】

2023每日刷题&#xff08;九十八&#xff09; Leetcode—114. 二叉树展开为链表 Morris-like算法思想 可以发现展开的顺序其实就是二叉树的先序遍历。算法和 94 题中序遍历的 Morris 算法有些神似&#xff0c;我们需要两步完成这道题。 将左子树插入到右子树的地方将原来的右…

PreNorm和PostNorm对比

要点总结 标准的Transformer使用的是PostNorm 在完全相同的训练设置下Pre Norm的效果要优于Post Norm&#xff0c;这只能显示出Pre Norm更容易训练&#xff0c;因为Post Norm要达到自己的最优效果&#xff0c;不能用跟Pre Norm一样的训练配置&#xff08;比如Pre Norm可以不加…