【Go语言测评】slice和map性能对比

556 阅读13分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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自动扩容初始化过度初始化
100480.453.8675.24
10004317567.4743.5
10000478101109533727
10000060818889370417608
1000000609307414340333686517
10000000471434961377975897567023

可以看到,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自动扩容初始化过度初始化
100502925772723
1000646292444928692
10000579427246518327372
100000594750731341825382567
10000001267064887678852791462175

可以看到合理初始化map可以有1倍左右的写性能提升,过度初始化影响不太大,稍微开大点也没事。

slice与map对比

既然分别对map与slice写进行了扩容的测试,那么我们对map与slice也横评一下。

扩容速度的对比

append num自动扩容初始化过度初始化
100480.453.8675.24
10004317567.4743.5
10000478101109533727
10000060818889370417608
1000000609307414340333686517
10000000471434961377975897567023
add map自动扩容初始化过度初始化
100502925772723
1000646292444928692
10000579427246518327372
100000594750731341825382567
10000001267064887678852791462175

从上面刚才那两个表格可以看出,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可以用指针删除,这样相当于加法运算,还能再快一倍