如何以固定容量切分Slice

假设有一个长度为1000的Slice,里面存了很多数据,为了高效地处理这些数据,我需要将它重新分割为10个长度为100的Slice,然后开10个Goroutine来并行处理。那么,如何以固定容量切分这个Slice呢?

简单写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type T struct {
V int
}

func IntChunk(origins []T, limit int) [][]T {
var v [][]T
for i := 0; i < len(origins); i += limit {
end := i + limit
if end > len(origins) {
end = len(origins)
}
v = append(v, origins[i:end])
}
return v
}

但这种写法并不高效:由于没有提前分配v的容量,执行append时如果v的容量不够,会自动将v的容量翻倍,这就导致了内存的重新分配以及多余的内存占用,而内存的分配相对而言是非常耗时的。

高效写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func IntChunkOptimized(origins []T, limit int) [][]T {
size := len(origins)/limit + 1
if len(origins)%limit == 0 {
size--
}
v := make([][]T, size)
for i := range v {
start := i * limit
end := start + limit
if end > len(origins) {
end = len(origins)
}
v[i] = origins[start:end]
}
return v
}

究竟有多大提升?写个Benchmark测试一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func BenchmarkIntChunk(b *testing.B) {
for _, size := range []int{10000, 100000} {
data := prepareData(size)
b.Run("origin-"+strconv.Itoa(size), func(b *testing.B) {
for _, limit := range []int{1, 10, 100, 1000} {
b.Run(strconv.Itoa(limit), func(b *testing.B) {
for i := 0; i < b.N; i++ {
IntChunk(data, limit)
}
})
}
})
b.Run("optimized-"+strconv.Itoa(size), func(b *testing.B) {
for _, limit := range []int{1, 10, 100, 1000} {
b.Run(strconv.Itoa(limit), func(b *testing.B) {
for i := 0; i < b.N; i++ {
IntChunkOptimized(data, limit)
}
})
}
})
}
}

func prepareData(size int) []T {
var v []T
rand.Seed(time.Now().UnixNano())
for ; size > 0; size-- {
v = append(v, T{
V: rand.Intn(100),
})
}
return v
}

在我的MacBook Pro(CPU: 2.7 GHz Intel Core i5, 内存: 8 GB 1867 MHz DDR3)上测试得到以下结果

图1

可见,改进后的方法提升了2~7倍的执行效率,效果显著!