Gitlib Gitlib
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)

Ravior

以梦为马,莫负韶华
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)
  • PHP

  • Golang

    • Golang语法特点总结
    • Golang内存逃逸分析
    • Golang创建临时文件
    • Golang日常开发总结
    • Golang性能分析工具pprof使用总结
    • Golang源码系列-Slice
      • 1. Slice实现原理
      • 2. Slice的使用方法
        • 2.1 使用make创建Slice
        • 2.2 使用数组创建Slice
        • 2.3 Slice 扩容
        • 2.4 注意事项
      • 3. Slice性能
    • Golang源码系列-sync.Map
    • Golang开发实践万字总结
  • Python

  • Javascript

  • 其他语言

  • 编程语言
  • Golang
Ravior
2021-04-18
目录

Golang源码系列-Slice

# 1. Slice实现原理

Slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。 接下来按照实际使用场景分别介绍其实现机制。

源码包中src/runtime/slice.go:slice定义了Slice的数据结构:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
1
2
3
4
5

切片的底层结构是一个结构体,对应有三个参数:

  • array: 是一个unsafe.Pointer指针,指向一个具体的底层数组
  • len: 指的是切片的长度
  • cap: 指的是切片的容量

slice

# 2. Slice的使用方法

# 2.1 使用make创建Slice

使用make来创建Slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即容量。

例如,语句slice := make([]int, 5, 10)所创建的Slice,结构如下图所示:

slice

该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可。

# 2.2 使用数组创建Slice

使用数组来创建Slice时,Slice将与原数组共用一部分内存。

例如,语句slice := array[5:7]所创建的Slice,结构如下图所示:

slice

切片从数组array[5]开始,到数组array[7]结束(不含array[7]),即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5。

数组和切片操作可能作用于同一块内存,这也是使用过程中需要注意的地方。

# 2.3 Slice 扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

例如,当向一个capacity为5,且length也为5的Slice再次追加1个元素时,就会发生扩容,如下图所示:

slice

扩容操作只关心容量,会把原Slice数据拷贝到新Slice,追加数据由append在扩容结束后完成。上图可见,扩容后新的Slice长度仍然是5,但容量由5提升到了10,原Slice的数据也都拷贝到了新Slice指向的数组中。

扩容容量的选择遵循以下规则:

  • 如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍
  • 如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍
  • 使用append()向Slice添加一个元素的实现步骤如下:
    • 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice.
    • 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice;将新元素追加进新Slice,Slice.len++,返回新的Slice。

# 2.4 注意事项

  • 创建切片时可根据实际需要预分配容量,尽量避免追加过程中扩容操作,有利于提升性能
  • 切片拷贝时需要判断实际拷贝的元素个数
  • 谨慎使用多个切片操作同一个数组,以防读写冲突
  • 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片

# 3. Slice性能

通过上面我们知道slice的扩容涉及到内存的拷贝,这样带来的好处是数据存储在连续内存上,比随机访问快很多,最直接的性能提升就是缓存命中率会高很多。

拷贝内存数据是有开销的, 而其中最大的开销不在 memmove 数据上,而是在开辟一块新内存malloc及之后的GC压力,拷贝连续内存是很快的,随着cap变大,拷贝总成本还是 O(N) ,只是常数大了。假如不想发生拷贝,那你就没有连续内存。此时随机访问开销会是:链表 O(N), 2倍增长块链 O(LogN), 二级表一个常数很大的 O(1)。

如果能大致知道所需的最大空间(在大部分时候都是的)时,在make的时候预留相应的 cap 就好。

如果需要的空间很大,而且每次都不确定,那就要在浪费内存和耗 CPU 在 malloc + gc 上做权衡。

#Golang#原理系列
上次更新: 2022/12/01, 11:09:34
Golang性能分析工具pprof使用总结
Golang源码系列-sync.Map

← Golang性能分析工具pprof使用总结 Golang源码系列-sync.Map→

最近更新
01
常用游戏反外挂技术总结
11-27
02
Golang开发实践万字总结
11-11
03
Redis万字总结
10-30
更多文章>
Theme by Vdoing | Copyright © 2011-2022 Ravior | 粤ICP备17060229号-3 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式