Golang底层原理剖析之slice类型与扩容机制
概述
前言
- 熟悉 slice 的底层数据结构 - 实际存储数据的array,当前长度len与容量cap
- slice的扩容机制 - 不严格来说,当长度小于1024时,cap翻倍;大于1024时,增加1/4
- slice 有很多特性与 map 一致 - 记住一点,代码中操作的slice和map只是上层的,实际存储数据的是array与hmap
通过代码学习底层
package main
import (
"fmt"
"unsafe"
)
func slice() {
fmt.Println("Slice Init")
var s []int
// Tip: 对比一下map和slice的make函数,前者在类型后可跟0个或1个参数,而后者是1个和2个参数
s = make([]int, 2)
fmt.Println(len(s), cap(s))
s = make([]int, 2, 4)
fmt.Println(len(s), cap(s))
// Tip: 元素个数小于cap时,append不会改变cap,只会增加len
fmt.Println("Slice Assign")
s[0] = 1
s[1] = 2
s = append(s, 4)
fmt.Println(len(s), cap(s))
// Tip: 元素个数超过cap时,会进行扩容
s = append(s, 8, 16)
fmt.Println(len(s), cap(s))
// Tip: Slice没有显式的删除语句
fmt.Println("Slice Delete")
s = append(s[0:1], s[2:]...)
fmt.Println(s)
fmt.Println("Slice Range")
for i, v := range s {
fmt.Println(i, v)
fmt.Printf("%p %p\n", &i, &v)
}
}
/*
slice 的源码部分
slice基础结构slice:
包括保存数据的array、长度len与容量cap
初始化函数makeslice:
math.MulUintptr:根据元素大小和容量cap,计算所需的内存空间
mallocgc: 分配内存, 32K作为一个临界值,小的分配在P的cache中,大的分配在heap堆中
扩容growslice:
当长度小于1024时,cap翻倍;大于1024时,增加1/4。 但这个并不是绝对的,会根据元素的类型尽心过一定的优化
拷贝slicecopy:
核心函数为memmove,from=>to移动size大小的数据,size为 元素大小 * from和to中长度较小的个数
拷贝slicestringcopy:
基本与上面类似,字符串的拷贝
*/
func sliceAddr() {
fmt.Println("Part 1")
var s = make([]int, 2, 2)
s[0] = 1
fmt.Println(unsafe.Pointer(&s[0]))
s[1] = 2
fmt.Println(unsafe.Pointer(&s[0]))
// Tip: 扩容后,slice的array的地址会重新分配
s = append(s, 3)
fmt.Println(unsafe.Pointer(&s[0]))
fmt.Println("Part 2")
// Tip: a虽然是一个新的地址,但指向的array是和a一致的
a := s[:2]
fmt.Printf("%p %p\n", &s, &a)
fmt.Println(unsafe.Pointer(&a[0]))
a[0] = 2
fmt.Println(a, s)
// Tip: 如果要进行slice拷贝,使用copy方法
b := make([]int, 2)
copy(b, s)//copy会重新分配底层array的内存
fmt.Printf("%p %p\n", &s, &b)
fmt.Println(unsafe.Pointer(&b[0]))
fmt.Println("Part 3")
// Tip: sNil的array指向nil,而sEmpty的array指向一个内存地址
var sNil []int//底层array指向nil
var sEmpty = make([]int, 0)//底层array指向一个内存地址,但是这个内存地址没有分配空间给它
fmt.Println(len(sNil), len(sEmpty), cap(sNil), cap(sEmpty))
}
详解
slice类型
在runtime下的slice.go可以看到
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice基础结构包括保存数据的array、长度len与容量cap,其底层是数组array
这里的ints[3]和ints[4]不能访问,否则属于越界访问,会引发panic
扩容机制
在runtime下的slice.go内有一个扩容growslice函数
扩容机制是
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
//growslice部分代码
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
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
//下面还有优化
根据元素的类型做一定的优化
比如新容量是3,int类型,则它需要申请24B的内存,此时它会向语言自身的内存管理模块去申请内存,而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了32B的内存,所以这个时候新容量变成4个了
//runtime下sizeclasses.go文件
go采用的是基于tcmalloc进行的内存分配,也就是go语言的内存管理模块。
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}