本文从源码角度学习 golang slice 的创建、扩容,深拷贝的实现。

内部数据结构

slice 仅有三个字段,其中array 是保存数据的部分,len 字段为长度,cap 为容量。

type slice struct {
array unsafe.Pointer // 数据部分
len int // 长度
cap int // 容量
}

通过下面代码可以输出空slice 的大小:

package main

import "fmt"
import "unsafe" func main() {
data := make([]int, 0, 3) // 24 len:8, cap:8, array:8
fmt.Println(unsafe.Sizeof(data)) // 我们通过指针的方式,拿到数组内部结构的字段值
ptr := unsafe.Pointer(&data)
opt := (*[3]int)(ptr) // addr, 0, 3
fmt.Println(opt[0], opt[1], opt[2]) data = append(data, 123) fmt.Println(unsafe.Sizeof(data)) shallowCopy := data[:1] ptr1 := unsafe.Pointer(&shallowCopy) opt1 := (*[3]int)(ptr1) fmt.Println(opt1[0])
}

创建

创建一个slice,其实就是分配内存。cap, len 的设置在汇编中完成。

下面的代码主要是做了容量大小的判断,以及内存的分配。

func makeslice(et *_type, len, cap int) unsafe.Pointer {
// 获取需要申请的内存大小
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
} // 分配内存
// 小对象从当前P 的cache中空闲数据中分配
// 大的对象 (size > 32KB) 直接从heap中分配
// runtime/malloc.go
return mallocgc(mem, et, true)
}

append

对于不需要内存扩容的slice,直接数据拷贝即可。

上面的DX 存放的就是array 指针,AX 是数据的偏移. 将 123 存入数组。

而对于容量不够的情况,就需要对slice 进行扩容。这也是slice 比较关心的地方。 (因为对于大slice,grow slice会影响到内存的分配和执行的效率)

func growslice(et *_type, old slice, cap int) slice {
// 静态分析, 内存扫描
// ... if cap < old.cap {
panic(errorString("growslice: cap out of range"))
} // 如果存储的类型空间为0, 比如说 []struct{}, 数据为空,长度不为空
if et.size == 0 {
return slice{unsafe.Pointer(&zerobase), old.len, cap}
} newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
// 如果新容量大于原有容量的两倍,则直接按照新增容量大小申请
newcap = cap
} else {
if old.len < 1024 {
// 如果原有长度小于1024,那新容量是老容量的2倍
newcap = doublecap
} else {
// 按照原有容量的1/4 增加,直到满足新容量的需要
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// 通过校验newcap 大于0检查容量是否溢出。
if newcap <= 0 {
newcap = cap
}
}
} var overflow bool
var lenmem, newlenmem, capmem uintptr
// 为了加速计算(少用除法,乘法)
// 对于不同的slice元素大小,选择不同的计算方法
// 获取需要申请的内存大小。 switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
// 二的倍数,用位移运算
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
// 其他用除法
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
} // 判断是否会溢出
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
} // 内存分配 var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
p = mallocgc(capmem, nil, false)
// 清空不需要数据拷贝的部分内存
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if writeBarrier.enabled { // gc 相关
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
} // 数据拷贝
memmove(p, old.array, lenmem) return slice{p, old.len, newcap}
}

切片拷贝 (copy)

切片的浅拷贝

    shallowCopy := data[:1]

    ptr1 := unsafe.Pointer(&shallowCopy)

    opt1 := (*[3]int)(ptr1)

    fmt.Println(opt1[0])

下面是上述代码的汇编代码:

上面,先将 data 的成员数据拷贝到寄存器,然后从寄存器拷贝到shallowCopy的对象中。(注意到只是拷贝了指针而已, 所以是浅拷贝)

切片的深拷贝

深拷贝也比较简单,只是做了一次内存的深拷贝。

func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
} n := fm.len
if to.len < n {
n = to.len
} // 元素大小为0,则直接返回
if width == 0 {
return n
} // 竟态分析和内存扫描
// ... size := uintptr(n) * width
// 直接内存拷贝
if size == 1 { // common case worth about 2x to do here
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
} // 字符串slice的拷贝
func slicestringcopy(to []byte, fm string) int {
if len(fm) == 0 || len(to) == 0 {
return 0
} n := len(fm)
if len(to) < n {
n = len(to)
} // 竟态分析和内存扫描
// ... memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}

其他

  1. 汇编的生成方法
go tool compile -N -S slice.go > slice.S
  1. 需要了解unsafe.Pointer 的使用

  2. slice.go 位于 runtime/slice.go

  3. 上述代码使用 go1.12.5 版本

  4. 还有一点需要提醒, type 长度为0的对象。比如说 struct{} 类型。(所以,很多使用chan struct{} 做channel 的传递,节省内存)

package main

import "fmt"
import "unsafe" func main() {
var data [100000]struct{}
var data1 [100000]int // 0
fmt.Println(unsafe.Sizeof(data))
// 800000
fmt.Println(unsafe.Sizeof(data1))
}

golang slice 源码解读的更多相关文章

  1. go 中 sort 如何排序,源码解读

    sort 包源码解读 前言 如何使用 基本数据类型切片的排序 自定义 Less 排序比较器 自定义数据结构的排序 分析下源码 不稳定排序 稳定排序 查找 Interface 总结 参考 sort 包源 ...

  2. underscore 源码解读之 bind 方法的实现

    自从进入七月以来,我的 underscore 源码解读系列 更新缓慢,再这样下去,今年更完的目标似乎要落空,赶紧写一篇压压惊. 前文 跟大家简单介绍了下 ES5 中的 bind 方法以及使用场景(没读 ...

  3. jQuery.Callbacks 源码解读二

    一.参数标记 /* * once: 确保回调列表仅只fire一次 * unique: 在执行add操作中,确保回调列表中不存在重复的回调 * stopOnFalse: 当执行回调返回值为false,则 ...

  4. 第二十四课:jQuery.event.remove,dispatch的源码解读

    本课还是来讲解一下jQuery是如何实现它的事件系统的.这一课我们先来讲一下jQuery.event.remove的源码解读. remove方法的目的是,根据用户传参,找到事件队列,从里面把匹配的ha ...

  5. nodeJS之eventproxy源码解读

    1.源码缩影 !(function (name, definition) { var hasDefine = typeof define === 'function', //检查上下文环境是否为AMD ...

  6. Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...

  7. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  8. go中panic源码解读

    panic源码解读 前言 panic的作用 panic使用场景 看下实现 gopanic gorecover fatalpanic 总结 参考 panic源码解读 前言 本文是在go version ...

  9. Vue 源码解读(3)—— 响应式原理

    前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...

随机推荐

  1. 题解 P1457 【城堡 The Castle】

    来讨论区大摇大摆地逛了一圈后,我发现竟然大家的代码 都很长 然而代码真的要写那么长吗 首先,来分析问题,1,2,4,8,这些数显然是有特点的,也许你已经想到了没错,它们都是2的次方数. 1是2的0次方 ...

  2. FTP服务器与客户端的安装与配置

    FTP服务器安装与配置 1. 切换到root用户:  su root 2. 查看系统中是否安装了vsftpd,可以通过执行命令 :  rpm -qa | grep vsftpd 3. 如果没有安装 v ...

  3. MySQL优化之慢查询日志

    慢查询日志概述 所谓慢查询日志,就是用于记录MySQL中响应时间超过设定阈值的SQL语句,通过打开慢查询开关,MySQL会将大于阈值的SQL记录在日志中,以便于分析性能. 慢查询日志选项默认是关闭的, ...

  4. Python Seaborn综合指南,成为数据可视化专家

    概述 Seaborn是Python流行的数据可视化库 Seaborn结合了美学和技术,这是数据科学项目中的两个关键要素 了解其Seaborn作原理以及使用它生成的不同的图表 介绍 一个精心设计的可视化 ...

  5. 看完这篇 Session、Cookie、Token,和面试官扯皮就没问题了

    Cookie 和 Session HTTP 协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录:Session 和 Cookie 的主要目的 ...

  6. SpringBoot中遇到的一些问题

    1.JQuery和bootstrap报404的问题 在html页面导入的js和css的时候,不要加static这级目录,直接跳过即可,例如 导入的时候不需要加static目录,直接导入js/和css/ ...

  7. 学习笔记-EL

    仅作为学习过程中笔记作用,若有不正确的地方欢迎指正 目标 理解El的作用,熟练使用EL EL表达式与Jsp表达式对比来记 EL表达式的概念,作用,语法 Jsp作用主要是用来实现动态网页的,而动态网页中 ...

  8. JAVA常见面试题问题简述(持续更新中)

    JAVA常见面试题问题简述 1. springcloud和dubbo的区别 ①相比之下springcloud 的社区会更加活跃,解决问题的速度也会越来越快,dubbo相对来说如果碰到没有解决的问题,就 ...

  9. WEB安全——XML注入

    浅析XML注入 认识XML DTD XML注入 XPath注入 XSL和XSLT注入 前言前段时间学习了.net,通过更改XML让连接数据库变得更方便,简单易懂,上手无压力,便对XML注入这块挺感兴趣 ...

  10. jenkins集成sonarQube实现代码质量检查

    1.sonarQube的简介 SonarQube是一款自动化代码审查工具,用于检测代码中的错误.漏洞和代码异味.它可以与你现有的工作流集成,以支持跨项目分支和拉取请求的连续代码检查. 其工作流程如下: ...