阅读、了解slice相关资料时,发现资料之间的内容各不相同,因此自己做个实验验证一下。
方案一:
struct{}
做值,创建长度(len)、容量(cap)都是 0
的切片append
向切片里面追加数据,计算 容量(cap) 变化和比例方案二:
int
做值,创建长度(len)、容量(cap)都是 0
的切片append
向切片里面追加数据,计算 容量(cap) 变化和比例实验环境:
go version go1.18beta1 windows/amd64
验证代码:
package main
import "fmt"
func main() {
s := make([]struct{}, 0)
c1 := cap(s)
tag1 := true
tag2 := true
for i := 0; i < 4096; i++ {
s = append(s, struct{}{})
if c1 != cap(s) {
if c1 == 0 {
fmt.Println("空数组")
} else if (c1*2 == cap(s)) && tag1 {
fmt.Printf("2倍成长 i=%d\r\n", i)
tag1 = false
} else if c1*2 > cap(s) && tag2 {
fmt.Printf("%f倍成长 cap [%d -> %d], i=%d\r\n", float64(cap(s))/float64(c1), c1, cap(s), i)
tag2 = false
} else {
fmt.Printf("%f倍成长 cap [%d -> %d], i=%d\r\n", float64(cap(s))/float64(c1), c1, cap(s), i)
}
}
c1 = cap(s)
}
}
输出内容:
空数组
2倍成长 i=1
1.500000倍成长 cap [2 -> 3], i=2
1.333333倍成长 cap [3 -> 4], i=3
1.250000倍成长 cap [4 -> 5], i=4
1.200000倍成长 cap [5 -> 6], i=5
... ...
1.000244倍成长 cap [4093 -> 4094], i=4093
1.000244倍成长 cap [4094 -> 4095], i=4094
1.000244倍成长 cap [4095 -> 4096], i=4095
验证代码:
package main
import "fmt"
func main() {
s := make([]int, 0)
c1 := cap(s)
tag1 := true
tag2 := true
for i := 0; i < 999999; i++ {
s = append(s, 1)
if c1 != cap(s) {
if c1 == 0 {
fmt.Println("空数组")
} else if (c1*2 == cap(s)) && tag1 {
fmt.Printf("2倍成长 i=%d\r\n", i)
tag1 = false
} else if c1*2 > cap(s) && tag2 {
fmt.Printf("%f倍成长 cap [%d -> %d], i=%d\r\n", float64(cap(s))/float64(c1), c1, cap(s), i)
tag2 = false
} else {
fmt.Printf("%f倍成长 cap [%d -> %d], i=%d\r\n", float64(cap(s))/float64(c1), c1, cap(s), i)
}
}
c1 = cap(s)
}
}
int
型切片输出内容:
空数组
2倍成长 i=1
2.000000倍成长 cap [2 -> 4], i=2
2.000000倍成长 cap [4 -> 8], i=4
2.000000倍成长 cap [8 -> 16], i=8
... ...
2.000000倍成长 cap [128 -> 256], i=128
2.000000倍成长 cap [256 -> 512], i=256
1.656250倍成长 cap [512 -> 848], i=512
1.509434倍成长 cap [848 -> 1280], i=848
... ...
1.250474倍成长 cap [539648 -> 674816], i=539648
1.250379倍成长 cap [674816 -> 843776], i=674816
1.251214倍成长 cap [843776 -> 1055744], i=843776
bool
型切片输出内容:
空数组
1.000000倍成长 cap [8 -> 8], i=1
2倍成长 i=8
2.000000倍成长 cap [16 -> 32], i=16
... ...
2.000000倍成长 cap [256 -> 512], i=256
1.750000倍成长 cap [512 -> 896], i=512
1.571429倍成长 cap [896 -> 1408], i=896
... ...
1.259259倍成长 cap [442368 -> 557056], i=442368
1.264706倍成长 cap [557056 -> 704512], i=557056
1.255814倍成长 cap [704512 -> 884736], i=704512
1.259259倍成长 cap [884736 -> 1114112], i=884736
string
型切片输出内容:
空数组
2倍成长 i=1
2.000000倍成长 cap [2 -> 4], i=2
2.000000倍成长 cap [4 -> 8], i=4
... ...
2.000000倍成长 cap [256 -> 512], i=256
1.656250倍成长 cap [512 -> 848], i=512
1.509434倍成长 cap [848 -> 1280], i=848
... ...
1.250903倍成长 cap [567296 -> 709632], i=567296
1.250361倍成长 cap [709632 -> 887296], i=709632
1.250433倍成长 cap [887296 -> 1109504], i=887296
float64
型切片输出内容:
空数组
2倍成长 i=1
2.000000倍成长 cap [2 -> 4], i=2
2.000000倍成长 cap [4 -> 8], i=4
... ...
2.000000倍成长 cap [256 -> 512], i=256
1.656250倍成长 cap [512 -> 848], i=512
1.509434倍成长 cap [848 -> 1280], i=848
... ...
1.250474倍成长 cap [539648 -> 674816], i=539648
1.250379倍成长 cap [674816 -> 843776], i=674816
1.251214倍成长 cap [843776 -> 1055744], i=843776
将计算次数调整为 2^32
,输出内容:
空数组
2倍成长 i=1
2.000000倍成长 cap [2 -> 4], i=2
... ...
2.000000倍成长 cap [128 -> 256], i=128
2.000000倍成长 cap [256 -> 512], i=256
1.656250倍成长 cap [512 -> 848], i=512
... ...
1.250000倍成长 cap [547171328 -> 683964416], i=547171328
1.250001倍成长 cap [683964416 -> 854956032], i=683964416
1.250001倍成长 cap [854956032 -> 1068695552], i=854956032
runtime: VirtualAlloc of 10686963712 bytes failed with errno=1455
fatal error: out of memory
runtime stack:
struct{}
)的空间每次加1,不按比例增加。1.25
(但不是绝对等于)结果表明:网上大多数博客、文章关于golang slice空间增长的相关都是有问题的,不能解释实验结果。
接下来需要分析源码。
源码位置:go/src/runtime/slice.go
。
关键函数:func growslice(et *_type, old slice, cap int) slice
源码总结:
5/4
等比的方式增加,直到超过目标slice的容量3.2.1
),旧切片容量仍然小于等于0,使用目标切片的cap(容量)关键源码解析:
func growslice(et *_type, old slice, cap int) slice {
// ... 忽略上面代码 ...
newcap := old.cap
doublecap := newcap + newcap // 容量翻倍
if cap > doublecap {
// 1. 如果翻倍以后的容量,还是比目标切片的cap(容量)少,直接使用目标切片的cap(容量)
newcap = cap
} else {
// 2. 翻倍以后的容量足够大
if old.cap < 1024 {
// 2.1 旧slice的容量小于1024,直接采用翻倍的值
newcap = doublecap
} else {
// 2.2 旧slice的容量大于等于1024,如果:
// 2.2.1 容量按照 `1/4` 等比数列的方式增加,直到超过目标slice的容量
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// 2.2.2 旧切片容量小于等于0,使用目标切片的cap(容量)
if newcap <= 0 {
newcap = cap
}
}
}
//... 忽略下面代码 ...
}
struct {}
,空间增长每次增加 1
2.3.1 容量按照 5/4
等比的方式增加,直到超过目标slice的容量
这里可以 等价成
比例为 5/4
的等比数列,注意,并不是计算一次,而是反复乘以 5/4
直到容量超过目标值。
所以严格来说新slice的空间并不是 1.25
倍,而是 以 1.25
为底的等比数列求和后的结果。
2.3.2 经过上一步处理后(2.3.2
),旧切片容量仍然小于等于0,使用目标切片的cap(容量)
上面说的都是切片的容量(cap),实际分配的内存和容量还受到内存对齐的影响。
关键源码函数:func makeslicecopy(et *_type, tolen int, fromlen int, from unsafe.Pointer) unsafe.Pointer
通过基础测试代码初步发现,并通过进阶测试代码进一步证实append时的内存对齐规律:
int64
: cap按照2的倍数对齐int32
:cap按照2的倍数对齐bool
:cap按照8的倍数对齐string
:没发现规律5 6 7 8 9 10 11 12 13 14 15 16 18 20 22 24 26 28 30 32 36 40 44 48 56 64 72 80 88 96 112 128 144 168 192 200 216 256 304 336 384 408 424 432 512 592 608 640 680 768 848 896 1024 1152 1192 1280 1360 1536 1704 1792 2048 2560
基础测试代码:
package main
import "fmt"
type Null struct {
}
func main() {
s0 := []Null{Null{}, Null{}}
s0 = append(s0,
[]Null{Null{}, Null{}, Null{}, Null{}, Null{}, Null{}, Null{}}... // 追加的切片,测试时里面的元素数量可以一个个的增加
)
fmt.Printf("len=%d, cap=%d\r\n", len(s0), cap(s0))
s := []int64{1, 2}
s = append(s, []int64{3, 4, 5, 6, 7, 8, 9}...)
fmt.Printf("len=%d, cap=%d\r\n", len(s), cap(s))
s2 := []bool{true, true}
s2 = append(s2, []bool{true, true, true, true, true, true, true}...)
fmt.Printf("len=%d, cap=%d\r\n", len(s2), cap(s2))
}
进阶测试代码:
package main
import "fmt"
type Null struct {
}
func main() {
n := 2048
s0 := []Null{Null{}, Null{}}
for i := 3; i < n; i++ {
if ap1(s0, make([]Null, i)) {
fmt.Printf("Null => 找到不满足条件的数据,在长度 %d 次时触发\r\n", i)
return
}
}
s1 := []int64{1, 2}
for i := 3; i < n; i++ {
if ap2(s1, make([]int64, i)) {
fmt.Printf("int64 => 找到不满足条件的数据,在追加长度 %d 时触发\r\n", i)
return
}
}
s3 := []int32{1, 2}
for i := 3; i < n; i++ {
if ap3(s3, make([]int32, i)) {
fmt.Printf("int32 => 找到不满足条件的数据,在追加长度 %d 次时触发\r\n", i)
return
}
}
s2 := []bool{true, true}
for i := 3; i < n; i++ {
if ap4(s2, make([]bool, i)) {
fmt.Printf("bool => 找到不满足条件的数据,在追加长度 %d 次时触发\r\n", i)
return
}
}
s4 := []string{"a", "b"}
for i := 3; i < n; i++ {
if ap5(s4, make([]string, i)) {
fmt.Printf("string => 找到不满足条件的数据,在追加长度 %d 次时触发\r\n", i)
return
}
}
}
func ap1(old, new []Null) bool {
old = append(old, new...)
//fmt.Printf("struct{} : len=%d, cap=%d\r\n", len(old), cap(old))
return cap(old) != len(old)
}
func ap2(old, new []int64) bool {
old = append(old, new...)
//fmt.Printf("int64 : len=%d, cap=%d\r\n", len(old), cap(old))
return (cap(old) % 2) != 0
}
func ap3(old, new []int32) bool {
old = append(old, new...)
//fmt.Printf("int32 : len=%d, cap=%d\r\n", len(old), cap(old))
return (cap(old) % 2) != 0
}
func ap4(old, new []bool) bool {
old = append(old, new...)
//fmt.Printf("bool : len=%d, cap=%d\r\n", len(old), cap(old))
return (cap(old) % 8) != 0
}
func ap5(old, new []string) bool {
old = append(old, new...)
//fmt.Printf("string : len=%d, cap=%d\r\n", len(old), cap(old))
return cap(old) != len(old)
}
本文由 qingchuwudi 译制或原创,除非另有声明,在不与原著版权冲突的前提下,本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。