一、前言
slice这种数据结构便于使用和管理数据集合,可以理解为是一种动态数组,slice也是围绕动态数组的概念来构建的。既然是动态数组,那么slice是如何扩容的呢?
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一 如果扩容之后,还没有触及原数组的容量,那么切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组 切片的动态增长是通过内置函数append来实现的,这个函数可以快速高效的增长切片,还可以通过对切片再次切片来缩小一个切片的大小。切片是一个非常小的对象,它是对底层的数组进行了抽象,并且提供了相关的操作方法。他拥有三个字段,分别为:指向底层数组的指针,长度,容量。
二、具体分析(结合源码)
[源码位置](runtime/slice.go growslice)
注:我们将扩容后的实际容量表示为newCap,根据append()的传入参数得到的需要的目标容量表示为cap,原切片表示为oldSlice,扩容后的切片表示为newSlice,切片中元素类型表示为et
- slice结合golang内置方法append进行动态扩容,具体实现方法为
func growslice(et *_type, old slice, cap int) slice;///传入的参数分别为切片中元素的类型,原来的切片,目标切片的容量
2.扩容
- golang首先会对目标容量进行判断,如果cap大于原来cap(oldSlice)的两倍,那么newCap就直接等于cap。
package main
import "fmt"
func main() {
//创建
slice := make([]int64, 3, 4)
fmt.Println(slice) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice), cap(slice)) // 3 5
// 添加元素,切片容量可以根据需要自动扩展
slice = append(slice, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10)
fmt.Println(slice) // [0 0 0 1 2 3 4 5 6 7 8 9 10 10 ]
fmt.Println(len(slice), cap(slice)) // 14 14
/*
原cap:oldcap = 5,len=3
新cap:s增加11个元素后,3+11=14,最小cap预估是14;
此时:oldcap*2 = 8,8<15;
那么新的cap就是15;
*/
}
- 对于cap小于或等于原来cap(oldSlice)的两倍,会有两种处理方式:
①、当cap(oldSlice)小于1024的时候,newCap会直接等于cap(oldSlice)的两倍
package main
import "fmt"
func main() {
//创建
slice := make([]int64, 3, 5)
fmt.Println(slice) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice), cap(slice)) // 3 5
// 添加元素,切片容量可以根据需要自动扩展
slice = append(slice, 1, 2, 3, 4)
fmt.Println(slice) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice), cap(slice)) // 7 10
/*
原cap:oldcap = 5,len=3
新cap:s增加四个元素后,3+4=7,最小cap预估是7;
此时:oldcap*2 = 10,10>7;
那么新的cap就是10;
*/
}
②、当cap(oldSlice)大于或等于1024的时候,会循环增加newCap的四分之一直到newCap大于或等于cap的时候,才停止循环。
源码体现:
大白话:
//按照这个顺序判断下去
if oldcap*2 <预估cap{
newcap = 预估cap
return newcap
}else{
//当旧容量*2大于等于预估cap的时候,对于旧容量则有以下几种可能
if oldcap > 预估cap{ //1、旧容量完全大于预估cap
newcap = oldcap
return newcap
}else{ //2、旧容量小于等于cap 但是大于等于预估cap/2
//当旧容量小于预估cap,&旧容量*2才能大于等于预估cap的时候
if oldlen <1024{
newcap = oldcap*2
return newcap
}
if oldlen>=1024{
newcap = oldcap*1.25
return newcap
}
}
}
- golang并不会完全把上面计算的newCap当作最终的newCap,而是要根据他的内存分配策略进行细微调整,从总的来说就是golang的内存对齐,我们接着下面来看一个例子。
// len=2 cap=3
s:=make([]int,2,3)
// 一次性增加五个元素
s=append(s,12,23,19,20,19)
/*
原cap:oldcap = 3,len=2
新cap:s增加五个元素后,2+5=7,最小cap预估是7;
此时:oldcap*2 = 6,6<7;
那么新的cap就是7;
按照常理,到的新切片 len=7,cap=7才对,
但是对于这种一次性塞入多个数据的,go对其进行了内存对齐。
首先:
原s占用的字节数:2*8=16
增加的五个元素占用的字节数:5*8=40
40+16=56
56个字节根据内存管理折合下来拿到的内存段是64字节大小的内存段,对应数量是8.
所以得到的实际上是len=7,cap=8
*/
- golang将这个策略结合切片中元素类型et具体细分为四种策略,但是核心理念就是需要将分配的内存进行对齐,具体实现为roundupSize方法 源码体现:
- golang先判断从上面计算出来的size=newCap*unsafe.size(et)的大小,是否为小对象,如果是小对象,则根据golang内存分配进行内存补齐,也就是说,打个比方,如果上面计算出来的size为60,那么根据内存补齐,他会变成64来进行分配。
golang先判断从上面计算出来的size=newCapunsafe.size(et)的大小,是否为小对象,如果是小对象,则根据golang内存分配进行内存补齐,也就是说,打个比方,如果上面计算出来的size为56,那么根据内存补齐,他会变成64来进行分配,对应数量是8。*
三、练习
package main
import "fmt"
func main() {
//创建
slice := make([]float32, 3, 5)
fmt.Println(slice) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice), cap(slice)) // 3 5
// 添加元素,切片容量可以根据需要自动扩展
slice = append(slice, 1, 2, 3, 4)
fmt.Println(slice) // [0, 0, 0, 1, 2, 3, 4]
fmt.Println(len(slice), cap(slice)) // 7 12
/*
原cap:oldcap = 5,len=3
新cap:s增加四个元素后,3+4=7,最小cap预估是7;
此时:oldcap*2 = 10;
如果oldcap*2>最小预估cap:7,
未完待续...
*/
}