append
是我们向切片添加元素时的首选函数,但这可能不是最好用法。原因如下:
首先,我们创建两个函数,功能是将字符 “x” 填充进一个字符串切片。
WithAppend 调用 append 将 “x” 添加到一个字符串切片中
1 2 3 4 5 6 7 8
| func WithAppend() []string { var l []string for i := 0; i < 100; i++ { l = append(l, "x") }
return l }
|
WithAssignAlloc 通过用 make 来创建一个指定大小的字符串切片,之后赋值 “x” 给指定索引位而不是使用 append
1 2 3 4 5 6 7 8
| func WithAssignAlloc() []string { l := make([]string, 100) for i := 0; i < 100; i++ { l[i] = "x" }
return l }
|
这两个函数返回相同的结果,但其实现方式完全不同。
现在,让我们对这些函数进行基准测试。
1 2 3 4 5 6 7 8 9 10 11 12
| func BenchmarkWithAppend(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { WithAppend() } } func BenchmarkWithAssignAlloc(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { WithAssignAlloc() } }
|
结果如下:
1 2 3 4
| BenchmarkWithAppend BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op BenchmarkWithAssignAlloc BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
|
WithAppend
的性能最差,而 WithAssignAlloc
的性能最好,这个结论应该可以说服你应该避免 append 了吧?
但先别急着走。
我们再写一个使用 append
的函数,并通过指定大小和容量来创建一个字符串切片。
1 2 3 4 5 6 7
| func WithAppendAlloc() []string { l := make([]string, 0, 100) for i := 0; i < 100; i++ { l = append(l, "x") } return l }
|
再次运行基准测试。
1 2 3 4 5 6
| BenchmarkWithAppend BenchmarkWithAppend-8 863949 1322 ns/op 4080 B/op 8 allocs/op BenchmarkWithAppendAlloc BenchmarkWithAppendAlloc-8 2543119 514 ns/op 1792 B/op 1 allocs/op BenchmarkWithAssignAlloc BenchmarkWithAssignAlloc-8 2343424 523 ns/op 1792 B/op 1 allocs/op
|
现在我们在 WithAppendAlloc
和 WithAssignAlloc
上得到了同样好的性能。
为什么 WithAppend
性能很差?在使用 WithAppend
往切片中添加元素时,当切片的容量不足时,需要创建一个新的更大的切片来对切片进行扩容,这导致多次分配。
在你优化代码之前,应该通过基准测试来找到代码中的瓶颈。上面的例子过于简化,你可能并不总是知道应该提前分配切片的大小。
另外,过早地进行性能调整可能会矫枉过正。