开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情
Slice 扩容
slice类似java里的arraylist、c++里的vector,底层实现就是变长数组。slice不指定大小的初始化,容量会随着append扩容。当然不会每加一个数就扩容一次,这样子复杂度为n^2,现实中,我打印了不断append时,容量,以及扩容比例:
0
扩容倍率 +Inf
容量: 1
扩容倍率 2
容量: 2
扩容倍率 2
容量: 4
扩容倍率 2
容量: 8
扩容倍率 2
容量: 16
扩容倍率 2
容量: 32
扩容倍率 2
容量: 64
扩容倍率 2
容量: 128
扩容倍率 2
容量: 256
扩容倍率 2
容量: 512
扩容倍率 1.65625
容量: 848
扩容倍率 1.509433962264151
容量: 1280
扩容倍率 1.4
容量: 1792
扩容倍率 1.4285714285714286
容量: 2560
扩容倍率 1.33125
容量: 3408
扩容倍率 1.5023474178403755
容量: 5120
扩容倍率 1.4
容量: 7168
扩容倍率 1.2857142857142858
容量: 9216
扩容倍率 1.3333333333333333
容量: 12288
扩容倍率 1.3333333333333333
容量: 16384
扩容倍率 1.3125
容量: 21504
扩容倍率 1.2857142857142858
容量: 27648
扩容倍率 1.2592592592592593
容量: 34816
扩容倍率 1.2647058823529411
容量: 44032
扩容倍率 1.255813953488372
容量: 55296
扩容倍率 1.2592592592592593
容量: 69632
扩容倍率 1.2647058823529411
容量: 88064
扩容倍率 1.255813953488372
容量: 110592
扩容倍率 1.2592592592592593
容量: 139264
扩容倍率 1.2573529411764706
容量: 175104
扩容倍率 1.2514619883040936
容量: 219136
扩容倍率 1.2523364485981308
容量: 274432
扩容倍率 1.2537313432835822
容量: 344064
扩容倍率 1.2529761904761905
容量: 431104
扩容倍率 1.2517814726840855
容量: 539648
扩容倍率 1.2504743833017078
容量: 674816
扩容倍率 1.2503793626707131
容量: 843776
扩容倍率 1.2512135922330097
容量: 1055744
扩容倍率 1.2502424830261882
容量: 1319936
扩容倍率 1.2505818463925524
容量: 1650688
扩容倍率 1.250620347394541
容量: 2064384
扩容倍率 1.2504960317460319
容量: 2581504
扩容倍率 1.25029750099167
容量: 3227648
扩容倍率 1.2503172588832487
容量: 4035584
扩容倍率 1.2501903070286728
容量: 5045248
扩容倍率 1.2500507408159123
容量: 6306816
扩容倍率 1.2500405910050332
容量: 7883776
扩容倍率 1.2500324717495779
容量: 9854976
扩容倍率 1.2501039068994182
容量: 12319744
扩容倍率 1.2500207796525642
容量: 15399936
扩容倍率 1.2500166234457077
容量: 19250176
扩容倍率 1.2500132985797117
容量: 24062976
扩容倍率 1.2500106387505852
容量: 30078976
扩容倍率 1.2500170218560631
容量: 37599232
扩容倍率 1.2500136172994172
容量: 46999552
扩容倍率 1.2500108937208594
容量: 58749952
扩容倍率 1.2500130723511058
容量: 73438208
扩容倍率 1.2500104577715185
容量: 91798528
扩容倍率 1.2500027887157406
容量: 114748416
可以看到0到512是很规律的扩容2倍,512到27648之间扩容1.33~1.5倍,之后就是每次扩容1/4。 如果每次扩容两倍,均摊时间复杂度就是f(n)=2n,Oavg(1) 如果每次扩容1/4,均摊时间复杂度就是f(n)=5n,Oavg(5) 所以理论上不容小觑,可以节省2到5倍的常数及时间。
性能对比测试
我们假设对Slice append num次,有三种情况:初始化大小为0,初始化大小为num,初始化大小为4num,分别对应没有初始化大小、刚刚好地初始化,和过分初始化大小。
func BenchmarkAutoGrow(b *testing.B) {
for i := 0; i < b.N; i++ {
s := []int{}
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}
func BenchmarkProperInit(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, numOfElems)
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}
func BenchmarkOverSizeInit(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, numOfElems*4)
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}
num=100的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 2532454 480.4 ns/op
BenchmarkProperInit-16 20997999 53.86 ns/op
BenchmarkOverSizeInit-16 16962544 75.24 ns/op
num=1000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 360836 4317 ns/op
BenchmarkProperInit-16 2169048 567.4 ns/op
BenchmarkOverSizeInit-16 1643751 743.5 ns/op
num=10000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 26554 47810 ns/op
BenchmarkProperInit-16 96840 11095 ns/op
BenchmarkOverSizeInit-16 38331 33727 ns/op
num=100000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 1866 608188 ns/op
BenchmarkProperInit-16 13524 89370 ns/op
BenchmarkOverSizeInit-16 3736 417608 ns/op
num=1000000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 194 6093074 ns/op
BenchmarkProperInit-16 813 1434033 ns/op
BenchmarkOverSizeInit-16 321 3686517 ns/op
num=10000000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 24 47143496 ns/op
BenchmarkProperInit-16 85 13779758 ns/op
BenchmarkOverSizeInit-16 13 97567023 ns/op
总结
整理得到表格:
| append num | 自动扩容 | 初始化 | 过度初始化 |
|---|---|---|---|
| 100 | 480.4 | 53.86 | 75.24 |
| 1000 | 4317 | 567.4 | 743.5 |
| 10000 | 47810 | 11095 | 33727 |
| 100000 | 608188 | 89370 | 417608 |
| 1000000 | 6093074 | 1434033 | 3686517 |
| 10000000 | 47143496 | 13779758 | 97567023 |
可以看到,slice初始化对性能提升是非常明显的,在1000以内甚至有10倍的性能提升,数据量大时也会有3~5倍的提升。 还有一点需要注意,空间的申请时间与申请大小成正比,必要过度申请太大的空间。
map 扩容
map与slice也有类似的扩容机制,为了减少哈希冲突的概率,随着map元素的增多,哈希表也在扩容。扩容时的空间申请、数据转移也是需要额外时间的,下面做类似的对比:
性能测试对比
和slice类似,分为三种:未初始化,准确初始化,过度初始化
func BenchmarkAutoGrow(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int)
for j := 0; j < numOfElems; j++ {
m[j] = j
}
}
}
func BenchmarkProperInit(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int, numOfElems)
for j := 0; j < numOfElems; j++ {
m[j] = j
}
}
}
func BenchmarkOverSizeInit(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int, 4*numOfElems)
for j := 0; j < numOfElems; j++ {
m[j] = j
}
}
}
num=100的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 214849 5029 ns/op
BenchmarkProperInit-16 482142 2577 ns/op
BenchmarkOverSizeInit-16 398781 2723 ns/op
num=1000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 18484 64629 ns/op
BenchmarkProperInit-16 48165 24449 ns/op
BenchmarkOverSizeInit-16 44281 28692 ns/op
num=10000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 2192 579427 ns/op
BenchmarkProperInit-16 4276 246518 ns/op
BenchmarkOverSizeInit-16 3500 327372 ns/op
num=100000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 199 5947507 ns/op
BenchmarkProperInit-16 370 3134182 ns/op
BenchmarkOverSizeInit-16 222 5382567 ns/op
num=1000000的结果
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkAutoGrow-16 8 126706488 ns/op
BenchmarkProperInit-16 15 76788527 ns/op
BenchmarkOverSizeInit-16 12 91462175 ns/op
总结
结果总结表格如下:
| add map | 自动扩容 | 初始化 | 过度初始化 |
|---|---|---|---|
| 100 | 5029 | 2577 | 2723 |
| 1000 | 64629 | 24449 | 28692 |
| 10000 | 579427 | 246518 | 327372 |
| 100000 | 5947507 | 3134182 | 5382567 |
| 1000000 | 126706488 | 76788527 | 91462175 |
可以看到合理初始化map可以有1倍左右的写性能提升,过度初始化影响不太大,稍微开大点也没事。
slice与map对比
既然分别对map与slice写进行了扩容的测试,那么我们对map与slice也横评一下。
扩容速度的对比
| append num | 自动扩容 | 初始化 | 过度初始化 |
|---|---|---|---|
| 100 | 480.4 | 53.86 | 75.24 |
| 1000 | 4317 | 567.4 | 743.5 |
| 10000 | 47810 | 11095 | 33727 |
| 100000 | 608188 | 89370 | 417608 |
| 1000000 | 6093074 | 1434033 | 3686517 |
| 10000000 | 47143496 | 13779758 | 97567023 |
| add map | 自动扩容 | 初始化 | 过度初始化 |
|---|---|---|---|
| 100 | 5029 | 2577 | 2723 |
| 1000 | 64629 | 24449 | 28692 |
| 10000 | 579427 | 246518 | 327372 |
| 100000 | 5947507 | 3134182 | 5382567 |
| 1000000 | 126706488 | 76788527 | 91462175 |
从上面刚才那两个表格可以看出,slice与map写入性能差距有一个数量级。特别是小数据量,差距更大,其主要原因就是hash和rehash的时间消耗。看来能用数组尽量别用map。例如对字母计数,开一个26长度slice即可,如果用map会慢非常多。
读取速度对比
就统计一下度的速度
func Benchmark_slice_read1000000(b *testing.B) {
m := make([]int, 1000000)
for j := 0; j < 100; j++ {
m[j] = j
}
b.StartTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 1000000; j++ {
a = m[j]
}
}
b.StopTimer()
}
func Benchmark_map_read1000000(b *testing.B) {
m := make(map[int]int, 1000000)
for j := 0; j < 1000000; j++ {
m[j] = j
}
b.StartTimer()
for i := 0; i < b.N; i++ {
for _, v := range m {
a = v
}
}
b.StopTimer()
}
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_slice_read100-16 42263044 29.04 ns/op
Benchmark_slice_read1000-16 5264535 231.5 ns/op
Benchmark_slice_read10000-16 466455 2276 ns/op
Benchmark_slice_read100000-16 560425 23720 ns/op
Benchmark_slice_read1000000-16 7556 234160 ns/op
Benchmark_map_read100-16 1627186 728.1 ns/op
Benchmark_map_read1000-16 129565 8534 ns/op
Benchmark_map_read10000-16 14606 79697 ns/op
Benchmark_map_read100000-16 1399 770675 ns/op
Benchmark_map_read1000000-16 122 9678057 ns/op
修改速度对比
所有元素加一
func Benchmark_slice_change1000000(b *testing.B) {
m := make([]int, 1000000)
b.StartTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 1000000; j++ {
m[j]++
}
}
b.StopTimer()
}
func Benchmark_map_change1000000(b *testing.B) {
m := make(map[int]int, 1000000)
b.StartTimer()
for i := 0; i < b.N; i++ {
for k := range m {
m[k]++
}
}
b.StopTimer()
}
pkg: learn/basic/prof/done/init/map
cpu: AMD Ryzen 7 6800H with Radeon Graphics
Benchmark_slice_change100-16 41984906 29.88 ns/op
Benchmark_slice_change1000-16 4622364 249.9 ns/op
Benchmark_slice_change10000-16 444596 2596 ns/op
Benchmark_slice_change100000-16 440690 27152 ns/op
Benchmark_slice_change1000000-16 3864 267596 ns/op
Benchmark_map_change100-16 770900 1523 ns/op
Benchmark_map_change1000-16 78681 15523 ns/op
Benchmark_map_change10000-16 10000 152095 ns/op
Benchmark_map_change100000-16 778 1551960 ns/op
Benchmark_map_change1000000-16 68 16491009 ns/op
删除速度对比
slice有三种删除元素的办法:
- 前缀删除切片
func Test_del(t *testing.T) {
for num := 100; num <= 100000000; num *= 10 {
m := make([]int, num)
for i := 0; i < num; i++ {
m[i] = i
}
start := time.Now()
for i := 0; i < num; i++ {
m = m[1:]
}
fmt.Printf("slice_del_pre_%v ", num)
fmt.Println(time.Since(start))
}
}
slice_del_pre_100 0s
slice_del_pre_1000 0s
slice_del_pre_10000 0s
slice_del_pre_100000 527.1µs
slice_del_pre_1000000 517.4µs
slice_del_pre_10000000 6.0191ms
slice_del_pre_100000000 51.3381ms
平均速度0.5ns/个
- 后缀删除切片
func Test_del(t *testing.T) {
for num := 100; num <= 100000000; num *= 10 {
m := make([]int, num)
for i := 0; i < num; i++ {
m[i] = i
}
start := time.Now()
for i := 0; i < num; i++ {
m = m[:len(m)-1]
}
fmt.Printf("slice_del_bak_%v ", num)
fmt.Println(time.Since(start))
}
}
slice_del_bak_100 0s
slice_del_bak_1000 0s
slice_del_bak_10000 0s
slice_del_bak_100000 523µs
slice_del_bak_1000000 522.6µs
slice_del_bak_10000000 3.505ms
slice_del_bak_100000000 46.9434ms
平均速度0.5ns/个
- 指针标记
func Test_del(t *testing.T) {
for num := 100; num <= 100000000; num *= 10 {
m := make([]int, num)
for i := 0; i < num; i++ {
m[i] = i
}
l := 0
start := time.Now()
for i := 0; i < num; i++ {
l++
}
fmt.Printf("slice_del_ptr_%v ", num)
fmt.Println(time.Since(start))
}
}
slice_del_ptr_100 0s
slice_del_ptr_1000 0s
slice_del_ptr_10000 0s
slice_del_ptr_100000 0s
slice_del_ptr_1000000 0s
slice_del_ptr_10000000 5.9029ms
slice_del_ptr_100000000 21.399ms
平均速度0.2ns/个
map直接delete删除kv
func Test_del(t *testing.T) {
for num := 100; num <= 100000000; num *= 10 {
m := make(map[int]int, num)
for j := 0; j < num; j++ {
m[j] = j
}
start := time.Now()
for k := range m {
delete(m, k)
}
fmt.Printf("map_del_%v ", num)
fmt.Println(time.Since(start))
}
}
map_del_100 0s
map_del_1000 0s
map_del_10000 0s
map_del_100000 0s
map_del_1000000 2.0305ms
map_del_10000000 14.1423ms
map_del_100000000 50.1673ms
平均速度0.5ns/个
总结
总结:
- 添加元素:map比slice慢10倍
- 读取元素:map比slice慢30倍
- 修改元素:map比slice慢50倍
- 删除元素:map和slice差不多,0.5ns/个
- slice可以用指针删除,这样相当于加法运算,还能再快一倍