02.Golang 切片(slice)源码分析(一、定义与基础操作实现)

Golang 切片(slice)源码分析(一、定义与基础操作实现)

注意当前go版本代码为1.23

一、定义

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

type slice struct {array unsafe.Pointerlen   intcap   int
}

数据结构如下:

切片数据结构

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

二、操作

2.1创建

src/runtime/slice.go 主要逻辑就是基于容量申请内存。

func makeslice(et *_type, len, cap int) unsafe.Pointer {// 计算切片所需的内存大小,cap 为切片容量,et.Size_ 为每个元素的大小// math.MulUintptr 执行无符号整数乘法,并返回结果和一个表示是否发生溢出的布尔值mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))// 检查以下几种错误情况:// 1. overflow:容量计算时发生溢出// 2. mem > maxAlloc:所需的内存超过最大可分配内存// 3. len < 0:切片长度为负数// 4. len > cap:切片长度大于容量if overflow || mem > maxAlloc || len < 0 || len > cap {// 注意:当用户执行 make([]T, bignumber) 时,产生 "len out of range" 错误,而不是 "cap out of range" 错误。// 虽然 "cap out of range" 也是对的,但由于容量是隐式提供的,因此提示长度错误更清晰。// 参考:golang.org/issue/4085// 重新计算基于长度的内存大小,以便在容量溢出时提供更准确的错误信息(例如 len 也溢出)mem, overflow := math.MulUintptr(et.Size_, uintptr(len))// 再次检查溢出、内存超出限制以及长度为负数的情况if overflow || mem > maxAlloc || len < 0 {panicmakeslicelen() // 长度错误,触发 panic}panicmakeslicecap() // 容量错误,触发 panic}// 调用 mallocgc 分配内存。// mallocgc 是 Go 运行时中的函数,用于分配内存并在垃圾回收器中注册该内存。// 参数://   mem: 要分配的内存大小//   et: 切片元素的类型//   true: 指示分配的内存应该被清零return mallocgc(mem, et, true)
}

2.2扩容

核心源码 growslice-》nextslicecap&&roundupsize

首先通过nextslicecap计算出一个初步的扩容值,通过roundupsize内存对齐得出最终的扩容值,并不是网上的简单公式。

在1.18版本之前,slice源码中nextslicecap扩容主要逻辑为:

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。

在1.18版本更新之后,slice源码中nextslicecap的扩容主要逻辑变为了:

当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4

下述为go1.23源码逻辑src/runtime/slice.go :

// growslice 为一个切片分配新的底层存储。
//
// 参数:
// oldPtr = 指向切片底层数组的指针
// newLen = 新的长度(= oldLen + num)
// oldCap = 原始切片的容量
// num = 正在添加的元素数量
// et = 元素类型
// 返回值:
// newPtr = 指向新底层存储的指针
// newLen = 新的长度与传参相同
// newCap = 新底层存储的容量
//
// 要求 uint(newLen) > uint(oldCap)。
// 假设原始切片的长度为 newLen - num
//
// 分配一个至少能容纳 newLen 个元素的新底层存储。
// 已存在的条目 [0, oldLen) 被复制到新的底层存储中。
// 添加的条目 [oldLen, newLen) 不会被 growslice 初始化
// (虽然对于包含指针的元素类型,它们会被清零)。调用者必须初始化这些条目。
// 尾随条目 [newLen, newCap) 被清零。
//
// growslice 的特殊调用约定使得调用此函数的生成代码更简单。特别是,它接受并返回
// 新的长度,这样旧的长度不需要被保留/恢复,而新的长度返回时也不需要被保留/恢复。
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {oldLen := newLen - num// ... 函数非核心部分省略if newLen < 0 {panic(errorString("growslice: len out of range"))}if et.Size_ == 0 {// append不应该创建一个指针为nil但长度非零的切片。// 我们假设在这种情况下,append不需要保留oldPtr。return slice{unsafe.Pointer(&zerobase), newLen, newLen}}// 1.计算新容量newcap := nextslicecap(newLen, oldCap)var overflow boolvar lenmem, newlenmem, capmem uintptr// 针对常见的 et.Size 进行优化// 对于1我们不需要任何除法/乘法。// 对于 goarch.PtrSize, 编译器会优化除法/乘法为一个常量移位。// 对于2的幂,使用变量移位。noscan := !et.Pointers()// 2.内存对齐switch {case et.Size_ == 1:lenmem = uintptr(oldLen)newlenmem = uintptr(newLen)capmem = roundupsize(uintptr(newcap), noscan)overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.Size_ == goarch.PtrSize:lenmem = uintptr(oldLen) * goarch.PtrSizenewlenmem = uintptr(newLen) * goarch.PtrSizecapmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)overflow = uintptr(newcap) > maxAlloc/goarch.PtrSizenewcap = int(capmem / goarch.PtrSize)case isPowerOfTwo(et.Size_):var shift uintptrif goarch.PtrSize == 8 {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, noscan)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)capmem = uintptr(newcap) << shiftdefault:lenmem = uintptr(oldLen) * et.Size_newlenmem = uintptr(newLen) * et.Size_capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))capmem = roundupsize(capmem, noscan)newcap = int(capmem / et.Size_)capmem = uintptr(newcap) * et.Size_}// ... 函数非核心部分省略
}核心代码:
// nextslicecap 计算下一个合适的切片容量。
// 该函数用于在切片需要扩容时,确定新的容量大小。
//  newLen: 切片的新长度(所需容量)。
//  oldCap: 切片的旧容量。
// 返回值: 新的切片容量。
func nextslicecap(newLen, oldCap int) int {newcap := oldCap // 将新的容量初始化为旧的容量doublecap := newcap + newcap // 计算旧容量的两倍// 如果新长度大于旧容量的两倍,则直接使用新长度作为新容量// 这是为了避免频繁的扩容操作,当所需长度远大于当前容量时,直接分配所需的空间if newLen > doublecap {return newLen}// 设置一个阈值,用于区分小切片和大切片const threshold = 256// 对于容量小于阈值的小切片,新容量直接设置为旧容量的两倍// 这是因为小切片的扩容成本相对较低if oldCap < threshold {return doublecap}// 对于容量大于等于阈值的大切片,采用更保守的扩容策略for {//  从2倍增长(小切片)过渡到1.25倍增长(大切片)。//  该公式在两者之间提供平滑的过渡。//  (newcap + 3*threshold) >> 2 等价于 (newcap + 3*threshold) / 4//  这使得新容量的增长比例在1.25到2之间,并随着切片容量的增大而逐渐接近1.25newcap += (newcap + 3*threshold) >> 2// 需要检查 `newcap >= newLen` 以及 `newcap` 是否溢出。// newLen 保证大于零,因此当 newcap 溢出时,`uint(newcap) > uint(newLen)` 不成立。// 这允许使用相同的比较来检查两者。// 使用uint类型进行比较是为了处理溢出情况。如果newcap溢出变成负数,转换为uint类型后会变成一个很大的正数,从而使得比较仍然有效。if uint(newcap) >= uint(newLen) {break // 如果新容量足够大,则退出循环}}//  当 newcap 计算溢出时,将 newcap 设置为请求的容量。//  如果 newcap 小于等于 0,说明发生了溢出if newcap <= 0 {return newLen}return newcap // 返回计算出的新容量
}// roundupsize 返回 mallocgc 为指定大小分配的内存块的大小,减去任何用于元数据的内联空间。
//  size: 请求分配的内存大小。
//  noscan:  如果为 true,则表示该内存块不需要垃圾回收扫描。
// 返回值: mallocgc 实际分配的内存块大小。
func roundupsize(size uintptr, noscan bool) (reqSize uintptr) {reqSize = size // 初始化请求大小// 处理小对象(小于等于 maxSmallSize-mallocHeaderSize)if reqSize <= maxSmallSize-mallocHeaderSize {// 小对象。// 如果需要垃圾回收扫描 (noscan 为 false) 并且大小大于 minSizeForMallocHeader,则添加 mallocHeaderSize 用于存储元数据。// heapBitsInSpan(reqSize) 用于检查对象是否足够小到可以存储在堆的位图中,如果可以,则不需要 mallocHeader。if !noscan && reqSize > minSizeForMallocHeader { // !noscan && !heapBitsInSpan(reqSize)reqSize += mallocHeaderSize}// (reqSize - size) 为 mallocHeaderSize 或 0。如果添加了 mallocHeaderSize,我们需要从结果中减去它,因为 mallocgc 会再次添加它。// 这里是为了确保返回的大小是 mallocgc 实际分配的大小,而不是包含了头部之后的大小。// 进一步区分更小的对象和中等大小的对象,使用不同的查找表进行向上取整if reqSize <= smallSizeMax-8 {// 对于非常小的对象,使用 size_to_class8 和 class_to_size 查找表进行向上取整,以 8 字节为粒度。// divRoundUp(reqSize, smallSizeDiv) 计算 reqSize 在 smallSizeDiv 粒度下的向上取整值。// class_to_size[...] 获取对应大小类的实际分配大小。// 最后减去 (reqSize - size) 移除之前可能添加的 mallocHeaderSize。return uintptr(class_to_size[size_to_class8[divRoundUp(reqSize, smallSizeDiv)]]) - (reqSize - size)}// 对于中等大小的对象,使用 size_to_class128 和 class_to_size 查找表进行向上取整,以 128 字节为粒度。return uintptr(class_to_size[size_to_class128[divRoundUp(reqSize-smallSizeMax, largeSizeDiv)]]) - (reqSize - size)}// 处理大对象(大于 maxSmallSize-mallocHeaderSize)// 大对象。将 reqSize 向上对齐到下一页。检查溢出。reqSize += pageSize - 1 // 将 reqSize 增加到下一页边界之前// 检查溢出。如果 reqSize 加上 pageSize - 1 后反而变小了,说明发生了溢出。if reqSize < size {return size // 返回原始大小,避免分配过大的内存}// 通过按位与运算将 reqSize 对齐到下一页边界。return reqSize &^ (pageSize - 1)
}

三、问题

【引申1】 [3]int 和 [4]int 是同一个类型吗?

不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。

【引申2】 下面的代码输出是什么?

说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。

package mainimport "fmt"func main() {slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := slice[2:5]s2 := s1[2:6:7]s2 = append(s2, 100)s2 = append(s2, 200)s1[2] = 20fmt.Println(s1)fmt.Println(s2)fmt.Println(slice)
}

结果:

[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]

s1slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

slice origin

接着,向 s2 尾部追加一个元素 100:

s2 = append(s2, 100) 

s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

append 100

再次向 s2 追加元素200:

s2 = append(s2, 200) 

这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

append 200

最后,修改 s1 索引为2位置的元素:

s1[2] = 20

这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。

s1[2]=20

再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

参考链接

1.Go 程序员面试笔试宝典

2.《Go学习笔记》

3.golangSlice的扩容规则

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

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

相关文章

记参加一次数学建模

题目请到全国大学生数学建模竞赛下载查看。 注&#xff1a;过程更新了很多文件&#xff0c;所有这里贴上的有些内容不是最新的&#xff08;而是草稿&#xff09;。 注&#xff1a;我们队伍并没有获奖&#xff0c;文章内容仅供一乐。 从这次比赛&#xff0c;给出以下赛前建议 …

virtualbox虚拟机中的ubuntu 20.04.6安装新的linux内核5.4.293 | 并增加一个系统调用 | 证书问题如何解决

参考文章&#xff1a;linux添加系统调用【简单易懂】【含32位系统】【含64位系统】_64位 32位 系统调用-CSDN博客 安装新内核 1. 在火狐下载你需要的版本的linux内核压缩包 这里我因为在windows上面下载过&#xff0c;配置过共享文件夹&#xff0c;所以直接复制粘贴通过共享文…

[Java实战]Spring Boot 3 整合 Ehcache 3(十九)

[Java实战]Spring Boot 3 整合 Ehcache 3&#xff08;十九&#xff09; 引言 在微服务和高并发场景下&#xff0c;缓存是提升系统性能的关键技术之一。Ehcache 作为 Java 生态中成熟的内存缓存框架&#xff0c;其 3.x 版本在性能、功能和易用性上均有显著提升。本文将详细介绍…

LlamaIndex 第九篇 Indexing索引

索引概述 数据加载完成后&#xff0c;您将获得一个文档对象(Document)列表&#xff08;或节点(Node)列表&#xff09;。接下来需要为这些对象构建索引(Index)&#xff0c;以便开始执行查询。 索引&#xff08;Index&#xff09; 是一种数据结构&#xff0c;能够让我们快速检索…

【问题排查】easyexcel日志打印Empty row!

问题原因 日志打印​​I/O 操作开销​&#xff08;如 Log4j 的 FileAppender&#xff09;会阻塞业务线程&#xff0c;直到日志写入完成&#xff0c;导致接口响应变慢 问题描述 在线上环境&#xff0c;客户反馈导入一个不到1MB的excel文件&#xff0c;耗时将近5分钟。 问题排…

代码随想录第51天|岛屿数量(深搜)、岛屿数量(广搜)、岛屿的最大面积

1.岛屿数量&#xff08;深搜&#xff09; ---》模板题 版本一写法&#xff1a;下一个节点是否能合法已经判断完了&#xff0c;传进dfs函数的就是合法节点。 #include <iostream> #include <vector> using namespace std;int dir[4][2] {0, 1, 1, 0, -1, 0, 0, -…

Made with Unity | 从影视到游戏:《鱿鱼游戏》IP 的边界拓展

优质IP的跨媒体开发潜力不可限量。以现象级剧集《鱿鱼游戏》为例&#xff0c;Netflix旗下游戏工作室Boss Fight在第二季开播前夕推出的手游《Squid Game: Unleashed》&#xff0c;一经发布便横扫全球107个国家和地区的App Store免费游戏榜首。 这款多人派对大逃杀游戏完美还原…

allure 报告更改标题和语言为中文

在网上看到好多谈到更改allure 的标题设置都很麻烦&#xff0c;去更改JSON文件 其实可以有更简单的办法&#xff0c;就是在生成报表时增加参数 使用allure --help 查看&#xff1a; --lang, --report-language 设置报告的语言&#xff0c;默认是应用 The report language. …

HGDB索引膨胀的检查与处理思路

文章目录 环境文档用途详细信息 环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;4.5.8 文档用途 本文档主要介绍HGDB索引膨胀的定义、产生的原因、如何检查以及遇到索引膨胀如何处理&#xff08;包括预防和解决&#xff09; 详细信息 …

【Python CGI编程】

Python CGI&#xff08;通用网关接口&#xff09;编程是早期Web开发中实现动态网页的技术方案。以下是系统化指南&#xff0c;包含核心概念、实现步骤及安全实践&#xff1a; 一、CGI 基础概念 1. 工作原理 浏览器请求 → Web服务器&#xff08;如Apache&#xff09; → 执行…

数据库故障排查指南:从入门到精通

1. 常见数据库故障类型 1.1 连接故障 数据库连接超时连接池耗尽网络连接中断认证失败1.2 性能故障 查询执行缓慢内存使用过高CPU使用率异常磁盘I/O瓶颈1.3 数据故障 数据不一致数据丢失数据损坏事务失败2. 故障排查流程 2.1 初步诊断 -- 检查数据库状态SHOW STATUS;SHOW PRO…

conda创建环境常用命令(个人用)

创建环境 conda create --name your_project_name创建环境 ---- 指定环境python版本 conda create --name your_project_name python3.x环境列表 conda env list激活环境 conda activate your_project_name退出环境 conda deactivate环境列表 #使用conda命令 conda list …

PCL 绘制二次曲面

文章目录 一、简介二、实现代码三、实现效果一、简介 这里基于二次曲面的公式: z = a 0 + a 1 x + a 2 y + a

一文讲透面向对象编程OOP特点及应用场景

面向对象编程&#xff08;Object-Oriented Programming, OOP&#xff09;是一种以对象为核心、通过类组织代码的编程范式。它通过模拟现实世界的实体和交互来构建软件系统&#xff0c;是现代软件开发中最广泛使用的范式之一。以下是 OOP 的全面解析&#xff1a; 一、OOP 的四大…

linux,我启动一个springboot项目, 用java -jar xxx.jar ,但是没多久这个java进程就会自动关掉

当使用 java -jar xxx.jar & 启动 Spring Boot 项目后进程自动关闭时&#xff0c;可能由多种原因导致。以下是常见排查步骤和解决方案&#xff1a; 一、查看日志定位原因 进程异常关闭通常会在控制台或日志中留下线索&#xff0c;建议先获取完整日志&#xff1a; 1. 查看…

【独家精简】win11(24h2)清爽加速版

自作该版本的初心&#xff1a;随着电脑性能的不断提升&#xff0c;我们需要的更多的是没有广告&#xff0c;没有推荐&#xff0c;没有收集隐私的windows清爽版纯净系统 目前只会去制作windows系统专业版 1、去除Windows系统自带的广告新闻和推荐以及小组间和聊天功能。 2、精简…

大二java第一面小厂(挂)

第一场&#xff1a; mybatis怎么防止数据转义。 Hutool用的那些你常用的方法。 springboot的常用注解。 redis的多级缓存。 websocket怎么实现的多人协作编辑功能。 怎么实现的分库分表。 mysql里面的各种操作&#xff0c;比如说分表怎么分&#xff0c;分页查询怎么用。 mybat…

OceanBase 的系统变量、配置项和用户变量有何差异

在继续阅读本文之前&#xff0c;大家不妨先思考一下&#xff0c;数据库中“系统变量”、“用户变量”以及“配置项”这三者之间有何不同。如果感到有些模糊&#xff0c;那么本文将是您理清这些概念的好帮手。 很多用户在使用OceanBase数据库中的“配置项”和“系统变量”&#…

HTML-3.3 表格布局(学校官网简易布局实例)

本系列可作为前端学习系列的笔记&#xff0c;代码的运行环境是在HBuilder中&#xff0c;小编会将代码复制下来&#xff0c;大家复制下来就可以练习了&#xff0c;方便大家学习。 系列文章目录 HTML-1.1 文本字体样式-字体设置、分割线、段落标签、段内回车以及特殊符号 HTML…

如何在Edge浏览器里-安装梦精灵AI提示词管理工具

方案一&#xff08;应用中心安装-推荐&#xff09;&#xff1a; 梦精灵 跨平台AI提示词管理工具 - Microsoft Edge AddonsMake Microsoft Edge your own with extensions that help you personalize the browser and be more productive.https://microsoftedge.microsoft.com…