Go 语言切片扩容规则是扩容2倍?1.25倍?到底几倍

本次主要来聊聊关于切片的扩容是如何扩的,还请大佬们不吝赐教

切片,相信大家用了 Go 语言那么久这这种数据类型并不陌生,但是平日里聊到关于切片是如何扩容的,很多人可能会张口就来,切片扩容的时候,如果老切片的容量小于 1024 那么就再扩容 1倍,也就是新的切片容量是老切片容量的两倍,同理,如果老切片容量大于 1024,那么就扩容1.25 倍

一个人这么说,多个人这么说,你可能就信了😂😂,可是大家都这么认为,我们就应该盲从吗?还是要自己去确认真实的扩容逻辑和实现方式,那就开始吧😁

结论先行,切片对于扩容并不一定是 2 倍,1.25倍,这个要看实际情况

本文分别从如下几点来聊聊切片的扩容

  • 扩容是针对切片的,数组无法扩容
  • 切片扩容到底是扩容到原来的几倍?
  • 我们一般使用切片的时候可以如何避免频繁的扩容?

扩容是针对切片的,数组无法扩容

首先需要明确,数组是不能扩容的,数组定义的时候就已经是定长的了,无法扩容

切片是可以扩容的,我们可以通过 append 追加的方式来向已有的切片尾部进行追加,若原有切片已满,那么就会发生扩容

另外,我们知道数组是一段连续的内存地址,同一种数据类型的数据集合,例如这样

func main() {log.SetFlags(log.Lshortfile)var demoArray = [5]int{1, 2, 3, 4, 5}log.Print("unsafe.sizeof(int) == ",unsafe.Sizeof(demoArray[0]))for i, _ := range demoArray {log.Printf("&demoAraay[%d] == %p", i, &demoArray[i])}}

可以看到在这个案例的环境中,一个 int 类型的变量占用 8 个字节,自然对于 demoArray 数组中,地址是连续的,每一个元素占用的空间也是我们所期望的

那么切片的数据地址也是连续的吗??

如果有人问这个问题,实际上是想问切片的底层数组的地址是不是也是连续的

我们知道,切片 slice 在 Go 中是一个结构体,其中 array 字段是一个指针,指向了一块连续的内存地址,也就是底层数组

type slice struct {array unsafe.Pointerlen   intcap   int
}

其中 len 字段记录了当前底层数组的实际有的元素个数,cap 表示底层数组的容量,自然也是切片slice 的容量

func main(){var demoSli = []int{1,2,3,4,5}log.Printf("len == %d,cap == %d",len(demoSli),cap(demoSli))for i, _ := range demoSli {log.Printf("&demoSli[%d] == %p", i, &demoSli[i])}
}

自然,demoSli 中的元素打印出来,地址也是连续的,没有毛病

此处 xdm 模拟的时候,切勿去打印拷贝值的地址,例如下面这种方式是相当不明智的

现在简单的去给 切片追加一个元素

可以看到切片的容量变成了原来的两倍(容量从 5 扩容成 10),且切片中底层数组的元素地址自然也是连续的,不需要着急下结论,继续往下看,好戏在后头

切片扩容到底是扩容到原来的几倍?

案例1 向一个cap 为 0 的切片中追加 2000 个元素,查看被扩容了几次

总共是扩容了 14 次

可以看到切片容量小于 1024 时,触发扩容都是扩容到原来的 2 倍,但是 大于 1024 之后,有的是 1.25 倍,有的是 1.35 倍,有的大于 1.35 倍,那么这是为什么呢?后面统一看源码

案例2 再次验证切片容量小于 1024,触发到扩容就一定是扩容 2 倍吗

  • 先初始化一个切片,里面有 5 个元素,len 为 5,cap 为 5
  • 再向切片中追加 6 个元素,分别是 6,7,8,9,10,11
  • 最终查看切片的容量是多少
func main(){var demoSli = []int{1, 2, 3, 4, 5}log.Printf("len == %d,cap == %d", len(demoSli), cap(demoSli))for i, _ := range demoSli {log.Printf("&demoSli[%d] == %p", i, &demoSli[i])}demoSli = append(demoSli,6,7,8,9,10,11)log.Printf("len == %d,cap == %d",len(demoSli),cap(demoSli))for i, _ := range demoSli {log.Printf("&demoSli[%d] == %p", i, &demoSli[i])}
}

通过这一段代码,我们可以看到,讲一个 len 为 5,cap 为 5 的切片,追加数字 6 的时候,切片应该要扩容到 10,然后追加到数字 11 的时候,切片应该扩容到 20,可实际真的是这样吗?

xdm 可以将上述 demo 贴到自己环境试试,得到的结果仍然会是切片的容量 cap 最终是 12,并不是 20

那么这一切都是为什么呢?我们来查看源码一探究竟

源码赏析

查看公共库中 runtime/slice.gogrowslice 函数就可以解开我们的疑惑

可以看出在我们使用 append 对切片追加元素的时候,实际上会调用到 growslice 函数, growslice 中的核心逻辑我们就可以理解为计算基本的 newcap 和进行字节对齐

  1. 进行基本的新切片容量计算
// 省略部分
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}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}
}
// 省略部分

此处逻辑可以知道

  • 如果当前传入的 cap 是比原有切片 cap 的 2 倍还要大,那么就会按照当前传入的 cap 来作为新切片的容量
  • 否则去校验原有切片的容量是否小于 1024

    • 若小于 1024 ,则按照原有的切片容量的 2 倍进行扩容
    • 若大于等于 1024 ,那么就按照原有切片的 1.25 倍继续扩容

然后是否看到这里就就结束了呢?就下定论来呢?并不,我们切莫断章取义,需要看全整个流程

  1. 进行基本的字节对齐

growslice 函数 计算出基本的 newcap 之后,还需要按照类型进行基本的字节对齐,此处字节对齐之后主要是 roundupsize 的函数实现,顺便将其涉及到的常量放到一起给大家展示一波

const (_MaxSmallSize = 32768smallSizeDiv = 8smallSizeMax = 1024largeSizeDiv = 128_NumSizeClasses = 68_PageShift = 13
)
func roundupsize(size uintptr) uintptr {if size < _MaxSmallSize {if size <= smallSizeMax-8 {return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])} else {return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])}}if size+ _PageSize < size {return size}return alignUp(size, _PageSize)
}func divRoundUp(n, a uintptr) uintptr {// a is generally a power of two. This will get inlined and// the compiler will optimize the division.return (n + a - 1) / a
}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, ...}

光看这个函数,没啥感觉,函数逻辑比较简单,就是基本的计算和索引,那么我们讲上述的案例2带入,来计算一下

此处很明确,当前旧的切片的 cap 为 5

也就是 growslice 函数 中 old.cap 为 5,传入的 cap 为 11,因此 cap > 2*old.cap

因此 newcap 此处等于 11

开始计算字节对齐之后的结果

  • roundupsize(uintptr(newcap) * sys.PtrSize) ,其中 newcap = 11,sys.PtrSize = 8,则 roundupsize 参数传入 88 ,此环境指针占用 8 字节
  • 按照如下逻辑进行计算

    • divRoundUp(88, 8) = 11
    • size_to_class8[11] = 8
    • class_to_size[8] = 96

此处环境我们的 int 类型是占用 8 个字节,因此最终的 newcap = 96/8 = 12

经过上述源码的处理,最终我们就可以正常的得到最终切片容量被扩容到 12 ,xdm 可以去看实际的源码

小结

使用 append 进行切片扩容的时候,先会按照基本的逻辑来计算 newcap 的大小

  • 如果当前传入的cap是比原有切片cap的2倍还要大,那么就会按照当前传入的cap来作为新切片的容量,否则去校验原有切片的容量是否小于 1024

  • 若小于1024,则按照原有的切片容量的2倍进行扩容

  • 若大于等于 1024,那么就按照原有切片的 1.25 倍继续扩容
    最终再进行字节对齐

那么实际上,最终的切片容量一般是会等于或者大于原有的 2倍 或者是 1.25 倍的

我们一般使用切片的时候可以如何避免频繁的扩容?

一般在使用切片的时候,尽量避免频繁的去扩容,我们可以对已知数据量的数据,进行一次性去分配切片的容量

例如,数据量有 1000 个,那么我们就可以使用 make 的方式来进行初始化

sli := make([]int, 0, 1000)

本次就是这样,如果对源码还挺感兴趣的话,xdm 可以去实际查看一下源码哦,希望对你有帮助

感谢阅读,欢迎交流,点个赞,关注一波 再走吧

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

文中提到的技术点,感兴趣的可以查看这些文章:

  • 你以为传切片就是传引用了吗?
  • 【切片】基础不扎实引发的问题
  • Go 语言中 panic 和 recover 搭配使用
  • Go 语言中的反射
  • 你真的知道 GO 中 nil 代表什么吗?
    可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI

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

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

相关文章

李沐深度学习记录5:13.Dropout

Dropout从零开始实现 import torch from torch import nn from d2l import torch as d2l# 定义Dropout函数 def dropout_layer(X, dropout):assert 0 < dropout < 1# 在本情况中&#xff0c;所有元素都被丢弃if dropout 1:return torch.zeros_like(X)# 在本情况中&…

electronjs入门-聊天应用程序,与Electron.js通信

随着第一章中构建的应用程序&#xff0c;我们将开始将其与Electron框架中的模块集成&#xff0c;并以此为基础&#xff0c;以更实用的方式了解它们。 过程之间的通信 根据第二章中的解释&#xff0c;我们将发送每个进程之间的消息&#xff1b;具体来说联系人和聊天&#xff1…

C++构造函数

在本文中&#xff0c;您将学习C 中的构造函数。您将学习什么是构造函数&#xff0c;如何创建它以及C 中的构造函数类型。 构造函数是成员函数的一种特殊类型&#xff0c;它在创建对象时会自动对其进行初始化。编译器通过其名称和返回类型将给定的成员函数标识为构造函数。构造函…

RabbitMQ开启消息发送确认和消费手动确认

开启RabbitMQ的生产者发送消息到RabbitMQ服务端的接收确认&#xff08;ACK&#xff09;和消费者通过手动确认或者丢弃消费的消息。 通过配置 publisher-confirm-type: correlated 和publisher-returns: true开启生产者确认消息。 server:port: 8014spring:rabbitmq:username: …

Reactor网络模式

文章目录 1. 关于Reactor模式的了解2. 基于Reactor模式实现epoll ET服务器2.1 EventItem类的实现2.2 Reactor类的实现Dispatcher函数AddEvent函数DelEvent函数EnableReadWrite函数 2.3 四个回调函数的实现acceptor回调函数recver回调函数sender回调函数errorer回调函数 3. epol…

mac使⽤nginx

⽅法1&#xff1a;homebrew 默认本地已经安装homebrew&#xff1b; 安装与启动 brew install nginx 安装nginx&#xff1b; brew services start nginx 启动nginx nginx⽂件⽬录 1. nginx安装⽂件⽬录/usr/local/Cellar/nginx 2. nginx配置⽂件⽬录/usr/local/etc/nginx 3. con…

【办公-excel】两个时间相减 (二) - 带毫秒的时间进行相减操作

一、使用内部函数 1.1 效果展示 TEXT(((RIGHT(TEXT(B2,"yyyy-mm-dd hh:mm:ss.000"),LEN(TEXT(B2,"yyyy-mm-dd hh:mm:ss.000"))-FIND(".",TEXT(B2,"yyyy-mm-dd hh:mm:ss.000")))-RIGHT(TEXT(A2,"yyyy-mm-dd hh:mm:ss.000"),…

微信支付v2

文档&#xff1a; https://pay.weixin.qq.com/wiki/doc/api/index.html 微信小程序&#xff1a;https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter11_1 需要一个微信认证后的小程序&#xff0c;&#xff0c;还需要一个&#xff0c;在微信商户平台&#xff0c;&…

jdbc(DriverManager+Connection+Statement+ResultSet)+SQL注入+开启预编译+数据连接池

1 JDBC概念 JDBC 就是使用Java连接并操作数据库的一套API 全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接 2 JDBC优势 可随时替换底层数据库&#xff0c;访问数据库的Java代码基本不变 以后编写操作数据库的代码只需要面向JDBC&#xff08;接口&#xf…

C++三大特性——继承(上篇)

文章目录 目录 一、继承的概念及定义 1.1继承的概念 1.2 继承定义 1.2.1定义格式 1.2.2继承关系和访问限定符 1.2.3继承基类成员访问方式的变化 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 一、继承的概念及定义 1.1继承的概念 继承(inherita…

最新AI智能创作系统源码AI绘画系统/支持GPT联网提问/支持Prompt应用

AI绘图专业设计 不得将程序用作任何违法违纪内容&#xff0c;不要让亲人两行泪 界面部分图解构&#xff1a; 前台show&#xff1a; 前端部署&#xff1a; 安装pm2管理器 点击设置 选择v16.19.1版本-切换版本 再新建一个网站 点击设置 添加反向代理-代理名称随便…

[MongoDB]-权限验证管理

[MongoDB]-权限验证管理 senge | 2023年9月 背景说明&#xff1a;现有两套MongoDB副本集群给开发人员使用时未开启认证。 产生影响&#xff1a;用户若输入账号以及密码则会进行校验&#xff0c;但用户可以在不输入用户名和密码的情况下也可直接登录。 倘若黑客借此进行攻击勒索…

ElasticSearch 学习7 集成ik分词器

网上找了一大堆&#xff0c;很多都介绍的不详细&#xff0c;开始安装完一直报错找不到plugin-descriptor.properties&#xff0c;有些懵这个东西不应该带在里面吗&#xff0c;参考了一篇博客说新建一个这个&#xff0c;新建完可以启动&#xff0c;但是插入索引数据会报错找不到…

Mini-dashboard 和meilisearch配合使用

下载的meilisearch一般是development模式&#xff0c;内置客户端&#xff0c;修改客户端后需要重要全部编译&#xff0c;花时间太长了。前后端分离才是正道&#xff0c;客户端修改不用重新编译后端。 方法如下&#xff1a; 1、修改配置文件/etc/meilisearch.toml&#xff0c;…

FPGA实现电机霍尔编码器模块

一. 简介 想要知道直流电机的转速&#xff0c;就需要用到编码器&#xff0c;常用的编码器有霍尔和光电两种&#xff0c;但是光电编码器比较贵(性能好于霍尔)&#xff0c;所以平常的时候使用最多的是霍尔编码器了。 霍尔编码器一般有AB两相信号输出&#xff0c;默认的时候为低…

基于springboot实现家具销售电商平台管理系统项目【项目源码+论文说明】

基于springboot实现家具销售电商平台管理系统演示 摘要 社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的交易方式逐渐受到广大人民群众的喜爱&#xff0c;也逐渐进入了每个用户的使用。互联网具有便利性&#xff0c;速度快&#xff0c;效率高&am…

ssti 前置学习

python venv环境 可以把它想象成一个容器&#xff0c;该容器供你用来存放你的Python脚本以及安装各种Python第三方模块&#xff0c;容器里的环境和本机是完全分开的 创建venv环境安装flask #apt install python3.10-venv #cd /opt #python3 -m venv flask1 #cd /opt 选…

信息增益,经验熵和经验条件熵——决策树

目录 1.经验熵 2.经验条件熵 3.信息增益 4.增益比率 5.例子1 6.例子2 在决策树模型中&#xff0c;我们会考虑应该选择哪一个特征作为根节点最好&#xff0c;这里就用到了信息增益 通俗上讲&#xff0c;信息增益就是在做出判断时&#xff0c;该信息对你影响程度的大小。比…

SpringCloud源码探析(十)-Web消息推送

1.概述 消息推送在日常使用中的场景比较多&#xff0c;比如有人点赞了我的博客或者关注了我&#xff0c;这时我就会收到一条推送消息&#xff0c;以此来吸引我点击或者打开应用。消息推送的方式主要分为两种&#xff1a;web消息推送和移动端消息推送。它将所要发送的信息&…

剑指offer——JZ84 二叉树中和为某一值的路径(三) 解题思路与具体代码【C++】

一、题目描述与要求 二叉树中和为某一值的路径(三)_牛客题霸_牛客网 (nowcoder.com) 题目描述 给定一个二叉树root和一个整数值 sum &#xff0c;求该树有多少路径的的节点值之和等于 sum 。 1.该题路径定义不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff…