Go Slice实现原理分析

认识 Slice

一种可变长度的数

操作

  • make :创建Slice,可以提前分配内存,
  • append:往Slice添加元素
package main
import ("fmt"
)func main() {slice := make([]int, 0, 1) // 7 runtime.makesliceslice = append(slice, 1) slice = append(slice, 2) // 9 runtime.growslicefmt.Println(slice, len(slice), cap(slice))
}
  • 汇编代码:go tool compile -S main.go
0x004c 00076 (main.go:7)        CALL    runtime.makeslice(SB) // 创建
0x0051 00081 (main.go:7)        MOVQ    24(SP), AX
0x0056 00086 (main.go:8)        MOVQ    $1, (AX)
0x005d 00093 (main.go:9)        LEAQ    type.int(SB), CX
0x0064 00100 (main.go:9)        MOVQ    CX, (SP)
0x0068 00104 (main.go:9)        MOVQ    AX, 8(SP)
0x006d 00109 (main.go:9)        MOVQ    $1, 16(SP)
0x0076 00118 (main.go:9)        MOVQ    $1, 24(SP)
0x007f 00127 (main.go:9)        MOVQ    $2, 32(SP)
0x0088 00136 (main.go:9)        CALL    runtime.growslice(SB) // 扩容
  • Slice 实现源码路径:runtime/slice.go,关注:runtime.makeslice, runtime.growslice。

源码实现 runtime/slice.go

Slice 结构

type slice struct {array unsafe.Pointer //数组首地址指针len   int //长度cap   int //容量
}

Slice 创建

func makeslice(et *_type, len, cap int) unsafe.Pointer {mem, overflow := math.MulUintptr(et.size, uintptr(cap)) // 判断申请大小是否超过限制, MulUintptr:主要就是用切片中元素大小和切片的容量相乘计算出所需占用的内存空间if overflow || mem > maxAlloc || len < 0 || len > cap {// NOTE: Produce a 'len out of range' error instead of a// 'cap out of range' error when someone does make([]T, bignumber).// 'cap out of range' is true too, but since the cap is only being// supplied implicitly, saying len is clearer.// See golang.org/issue/4085.mem, overflow := math.MulUintptr(et.size, uintptr(len))if overflow || mem > maxAlloc || len < 0 {panicmakeslicelen()}panicmakeslicecap()}return mallocgc(mem, et, true) // 内存申请// *_type 是 Go 中类型的实现type _type struct {size       uintptr // 大小ptrdata    uintptr // size of memory prefix holding all pointershash       uint32tflag      tflagalign      uint8fieldAlign uint8kind       uint8// function for comparing objects of this type// (ptr to object A, ptr to object B) -> ==?equal func(unsafe.Pointer, unsafe.Pointer) bool// gcdata stores the GC type data for the garbage collector.// If the KindGCProg bit is set in kind, gcdata is a GC program.// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.gcdata    *bytestr       nameOffptrToThis typeOff
}

内存分配

  • runtime.mspan:内存管理单元
  • runtime.mcache:线程缓存
  • runtime.mcentral :中心缓存
  • runtime.mheap:页堆
// mallocgc 内存申请
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {mp := acquirem()mp.mallocing = 1c := gomcache()var x unsafe.Pointernoscan := typ == nil || typ.ptrdata == 0if size <= maxSmallSize {if noscan && size < maxTinySize {// 微对象分配} else {// 小对象分配}} else {// 大对象分配}publicationBarrier()mp.mallocing = 0releasem(mp)return x
}
  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;
内存结构图

出自:Go 语言切片的实现原理| Go 语言设计与实现 - 面向信仰编程

Slice 扩容

package main
import ("fmt"
)func main() {slice := make([]int, 0, 2)slice = append(slice, 1)fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice))slice = append(slice, 2)fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice))slice = append(slice, 3) fmt.Printf("%p %d %d\n", unsafe.Pointer(&slice[0]), len(slice), cap(slice))
}
// 结果
0xc0000b4010 1 2 // 地址没变,长度:1,容量:2
0xc0000b4010 2 2 // 地址没变,长度:2,容量:2
0xc0000ba020 3 4 // 地址改变,长度:3,容量:4
  • 为什么结果会这样?
  • runtime.growslice 做了什么?为什么地址变了,容量也自动扩大了2倍?
0x025e 00606 (main.go:13)       PCDATA  $1, $0
0x025e 00606 (main.go:13)       NOP
0x0260 00608 (main.go:13)       CALL    runtime.growslice(SB) // slice = append(slice, 3)
0x0265 00613 (main.go:13)       MOVQ    40(SP), AX
0x026a 00618 (main.go:13)       MOVQ    48(SP), CX
0x026f 00623 (main.go:13)       MOVQ    56(SP), DX
0x0274 00628 (main.go:13)       MOVQ    $3, 16(AX)
func growslice(et *_type, old slice, cap int) slice {......if cap < old.cap {panic(errorString("growslice: cap out of range"))}if et.size == 0 {// append should not create a slice with nil pointer but non-zero len.// We assume that append doesn't need to preserve old.array in this case.return slice{unsafe.Pointer(&zerobase), old.len, cap}}newcap := old.cap// 1doublecap := newcap + newcap// 1+1 = 2 为什么不直接*2, 而是使用加法?if cap > doublecap {newcap = cap} else {if old.len < 1024 {newcap = doublecap} else {// Check 0 < newcap to detect overflow// and prevent an infinite loop.for 0 < newcap && newcap < cap {newcap += newcap / 4 // 1.25倍}// Set newcap to the requested cap when// the newcap calculation overflowed.if newcap <= 0 {newcap = cap}}}...switch {case et.size == 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif 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) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}...memmove(p, old.array, lenmem)
}//
func roundupsize(size uintptr) uintptr {if size < _MaxSmallSize { // 32768if 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)
}// alignUp rounds n up to a multiple of a. a must be a power of 2.
func alignUp(n, a uintptr) uintptr {return (n + a - 1) &^ (a - 1)
}const _MaxSmallSize   = 32768
const  smallSizeDiv    = 8
const  smallSizeMax    = 1024
const largeSizeDiv    = 128
  • 当原slice的cap小于1024时,新slice的cap变为原来的2倍;原slice的cap大于1024时,新slice变为原来的1.25倍
  • roundupsize是内存对齐的过程,我们知道golang中内存分配是根据对象大小来配不同的mspan,为了避免造成过多的内存碎片,slice在扩容中需要对扩容后的cap容量进行内存对齐的操作‘
下面是一个Slice 扩容简单流程

// Implementations are in memmove_*.s.
//
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)// memmove_amd64.s // 汇编实现的 memmove

常见操作以及带来的问题

Slice创建

make 到底带不带 cap?,怎么设置?

package main
import "fmt"func MakeCap(){s := make([]int, 0, 100000)for i:=0;i<100000;i++{s = append(s ,i)}
}func MakNoCap(){s := make([]int, 0)for i:=0;i<100000;i++{s = append(s ,i)}
}

压力测试代码

// Bench
package mainimport ("testing"
)
func BenchmarkMakeCap(b *testing.B) {for i := 0; i < b.N; i++ {MakeCap()}
}func BenchmarkMakNoCap(b *testing.B) {for i := 0; i < b.N; i++ {MakNoCap()}
}
// 结果
goos: darwin
goarch: amd64
pkg: test
BenchmarkMakeCap-16                10668            115607 ns/op
BenchmarkMakNoCap-16                2476            485026 ns/op
PASS
ok      test    4.166s

结论

  • 可以看到,指定容量和未指定容量效率相差近4倍,所以在Slice 初始化的时候,我们应该指定容量大小以提高效率
思考:newSlice := slice[0:1:1],这是什么操作?

Slice截取

package mainimport ("fmt""reflect""unsafe"
)func Slice(s []int) *reflect.SliceHeader {sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))return sh
}func main() {slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}s1 := slice[2:5]s2 := s1[2:7]fmt.Println("--------slice, s1, s2 init----------------")fmt.Printf("slice data addr=%d s1 data addr=%d s2 data addr=%d \n", Slice(slice).Data, Slice(s1).Data, Slice(s2).Data)fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice)fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1)fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2)fmt.Println("--------s2 append 100----------------")s2 = append(s2, 100)fmt.Printf("slice data addr=%d s1 data addr=%d s2 data addr=%d \n", Slice(slice).Data, Slice(s1).Data, Slice(s2).Data)fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice)fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1)fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2)fmt.Println("--------s2 append 200----------------")s2 = append(s2, 200)fmt.Printf("slice data addr=%d s1 data addr=%d s2 data addr=%d \n", Slice(slice).Data, Slice(s1).Data, Slice(s2).Data)fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice)fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1)fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2)fmt.Println("--------s1 modify [2]----------------")s1[2] = 20fmt.Printf("slice data addr=%d s1 data addr=%d s2 data addr=%d \n", Slice(slice).Data, Slice(s1).Data, Slice(s2).Data)fmt.Printf("len=%-4d cap=%-4d slice=%-1v \n", len(slice), cap(slice), slice)fmt.Printf("len=%-4d cap=%-4d s1=%-1v \n", len(s1), cap(s1), s1)fmt.Printf("len=%-4d cap=%-4d s2=%-1v \n", len(s2), cap(s2), s2)
}

结果

//
--------slice, s1, s2 init----------------
slice data addr=824633819296 s1 data addr=824633819312 s2 data addr=824633819328
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 9]
len=3    cap=8    s1=[2 3 4]
len=5    cap=6    s2=[4 5 6 7 8]
--------s2 append 100----------------
slice data addr=824633819296 s1 data addr=824633819312 s2 data addr=824633819328
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100]
len=3    cap=8    s1=[2 3 4]
len=6    cap=6    s2=[4 5 6 7 8 100]
--------s2 append 200----------------
slice data addr=824633819296 s1 data addr=824633819312 s2 data addr=824634196160 // 因为扩容,s1 底层数据地址变化,
len=10   cap=10   slice=[0 1 2 3 4 5 6 7 8 100]
len=3    cap=8    s1=[2 3 4]
len=7    cap=12   s2=[4 5 6 7 8 100 200]
--------s1 modify [2]----------------
slice data addr=824633819296 s1 data addr=824633819312 s2 data addr=824634196160
len=10   cap=10   slice=[0 1 2 3 20 5 6 7 8 100]
len=3    cap=8    s1=[2 3 20]
len=7    cap=12   s2=[4 5 6 7 8 100 200]
  • --------slice, s1, s2 init----------------

slice, s1, s2, 这三个Slice 都共用一个底层的数据

  • --------s2 append 100----------------

s2 往后面append 100,此时slice 中的 9 被替换为 100。

  • --------s2 append 200----------------

s2 继续添加 200,发生扩容,此时生成了新的底层数组,s2 不再与 slice, s1, 共用一个底层数据

  • --------s1 modify 2----------------

s1 修改2, slice 2 也会跟着变化,因为 slice, s1, 还是共用一个底层数据

如何避免这个问题?使用 Slice深拷贝
package mainimport "fmt"func main() {// Creating slices
slice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice2 []int
slice3 := make([]int, 5)// Before copying
fmt.Println("------------before copy-------------")
fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3)// Copying the slices
copy1 := copy(slice3, slice1)
copy2 := copy(slice2, slice1)
fmt.Println("------------after copy-------------")
fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3)
fmt.Println("slice1 --> slice3 total number of elements copied:", copy1)
fmt.Println("slice1 --> slice2 total number of elements copied:", copy2)
slice3 = append(slice3, 200)
fmt.Println("------------slice3 append 200-------------")
fmt.Printf("len=%-4d cap=%-4d slice1=%v\n", len(slice1), cap(slice1), slice1)
fmt.Printf("len=%-4d cap=%-4d slice2=%v\n", len(slice2), cap(slice2), slice2)
fmt.Printf("len=%-4d cap=%-4d slice3=%v\n", len(slice3), cap(slice3), slice3)}
结果
// ------------before copy-------------
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
len=5    cap=5    slice3=[0 0 0 0 0]
------------after copy-------------
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
len=5    cap=5    slice3=[0 1 2 3 4]
slice1 --> slice3 total number of elements copied: 5
slice1 --> slice2 total number of elements copied: 0
------------slice3 append 200-------------
len=10   cap=10   slice1=[0 1 2 3 4 5 6 7 8 9]
len=0    cap=0    slice2=[]
len=6    cap=10   slice3=[0 1 2 3 4 200]
  • 思考:为什么 copy2 := copy(slice2, slice1) 没有被slice1 copy到 slice2?

值传递还是引用传递

package mainimport "fmt"func main() {fmt.Println("slice init")slice := make([]int, 0, 10)fmt.Println(slice, len(slice), cap(slice))fmt.Println("slice append 1")slice = append(slice, 1)fmt.Println(slice, len(slice), cap(slice))fmt.Println("fn modify [0]")fn(slice)fmt.Println(slice, len(slice), cap(slice))fmt.Println("fn2 append 100 ele")fn2(slice)fmt.Println(slice, len(slice), cap(slice))
}
func fn(in []int) {in[0] = 100
}func fn2(in []int) {for i:=0;i<11;i++{in = append(in, i)}fmt.Println("fn2")fmt.Println(in, len(in), cap(in))fmt.Println("fn2")
}

结果

slice init
[] 0 10
slice append 1
[1] 1 10
fn modify [0]
[100] 1 10
fn2 append 100 ele
fn2
[100 0 1 2 3 4 5 6 7 8 9 10] 12 20 // fn2 里面的值改变了
fn2
[100] 1 10
  • fn 直接修改了 底层数组的值,所以会影响原先的slice
  • fn2 也修改了 底层数组的值,但是因为发生的扩容,这个时候已经是一个新的底层数组了,所以,原先的slice并没有受到影响 // 小心这里会产生bug
  • 结论:值传递,Go当中只有值传递

参考

  • Go 语言切片的实现原理| Go 语言设计与实现 - 面向信仰编程
  • 【Golang源码系列】二:Slice实现原理分析
  • 为什么要内存对齐

Go Slice实现原理分析相关推荐

  1. Java NIO使用及原理分析

    http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一些Java方面的开发,其中最重要的一块 ...

  2. 可视化拖拽组件库一些技术要点原理分析(三)

    本文是可视化拖拽系列的第三篇,之前的两篇文章一共对 17 个功能点的技术原理进行了分析: 编辑器 自定义组件 拖拽 删除组件.调整图层层级 放大缩小 撤消.重做 组件属性设置 吸附 预览.保存代码 绑 ...

  3. RPC 实战与核心原理分析

    RPC 实战与核心原理分析 RPCX是一个分布式的Go语言的 RPC 框架,支持Zookepper.etcd.consul多种服务发现方式,多种服务路由方式, 例子 服务端 package maini ...

  4. java signature 性能_Java常见bean mapper的性能及原理分析

    背景 在分层的代码架构中,层与层之间的对象避免不了要做很多转换.赋值等操作,这些操作重复且繁琐,于是乎催生出很多工具来优雅,高效地完成这个操作,有BeanUtils.BeanCopier.Dozer. ...

  5. Select函数实现原理分析

    转载自 http://blog.chinaunix.net/uid-20643761-id-1594860.html select需要驱动程序的支持,驱动程序实现fops内的poll函数.select ...

  6. spring ioc原理分析

    spring ioc原理分析 spring ioc 的概念 简单工厂方法 spirng ioc实现原理 spring ioc的概念 ioc: 控制反转 将对象的创建由spring管理.比如,我们以前用 ...

  7. 一次 SQL 查询优化原理分析(900W+ 数据,从 17s 到 300ms)

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:Muscleape jianshu.com/p/0768eb ...

  8. 原理分析_变色近视眼镜原理分析

    随着眼镜的发展,眼镜的外型变得越来越好看,并且眼镜的颜色也变得多姿多彩,让佩戴眼镜的你变得越来越时尚.变色近视眼镜就是由此产生的新型眼镜.变色镜可以随着阳光的强弱变换不同的色彩. 变色眼镜的原理分析 ...

  9. jieba分词_从语言模型原理分析如何jieba更细粒度的分词

    jieba分词是作中文分词常用的一种工具,之前也记录过源码及原理学习.但有的时候发现分词的结果并不是自己最想要的.比如分词"重庆邮电大学",使用精确模式+HMM分词结果是[&quo ...

最新文章

  1. [原创]CentOS下Mysql双机互为备份
  2. 独立开放者入行之前应该知道的8件事
  3. 不得不存!UI设计新手不可错过的7条实用法则
  4. python扇贝单词书_Python脚本 扇贝单词书爬取
  5. line-height:1.5和line-height:150%的区别
  6. JAVA_java.util.Date与java.sql.Date相互转换
  7. Flutter 成功在鸿蒙上运行;微信 8.0 发布;支付宝和微信支付达到反垄断标准 | 极客头条...
  8. 我的第一个全栈 Web 应用程序
  9. risc 服务器 操作系统,数据中心系统用RISC还是CISC?
  10. js中style.display=无效的解决方法
  11. Python开发Day07(学生选课)
  12. phpspider 爬取汉谜网
  13. android自定义popwindow,Android应用开发Android 自定义PopWindow的简单使用
  14. android 输入法 智能abc 风格,常见输入法智能ABC介绍5
  15. 【RPC Dubbo】本地存根和本地伪装
  16. 全面了解信贷业务流程
  17. [英语] 自建专业词典
  18. mybatis的association以及collection的用法
  19. 学计算机有什么好处和坏处,学习电脑有什么好处和坏处,电脑好处和坏处有哪些?...
  20. PCL:点云平移、旋转

热门文章

  1. 红帽RHCE考试下午-RHCE (RH294)任务概览[2021最新版]
  2. JAVA登录界面学生和老师_学生信息管理系统之第三篇登录界面java代码
  3. 京东手机评论分析(一):词云
  4. 拽一个贵人出来给你当炮架子
  5. JAVA毕设项目中小型企业资金流管理系统(java+VUE+Mybatis+Maven+Mysql)
  6. CentOS 7 中 pptpd安装
  7. 给华为服务器RH2288V3(hm23-03)安装驱动
  8. 蔡学镛[散文随笔]:从A到E+
  9. 无人机实时流怎么开_直播解决方案,如何利用无人机进行直播
  10. 就让这大雨全都落下 - 容祖儿