Golang 切片追加 vs 分配性能
Golang slice append vs assign performance
为了使切片追加操作更快,我们需要分配足够的容量。有两种附加切片的方法,代码如下:
func BenchmarkSliceAppend(b *testing.B) {
a := make([]int, 0, b.N)
for i := 0; i < b.N; i++ {
a = append(a, i)
}
}
func BenchmarkSliceSet(b *testing.B) {
a := make([]int, b.N)
for i := 0; i < b.N; i++ {
a[i] = i
}
}
结果是:
BenchmarkSliceAppend-4 200000000 7.87 ns/op 8 B/op 0 allocs/op
BenchmarkSliceSet-4 300000000 5.76 ns/op 8 B/op
为什么 a[i] = i
比 a = append(a, i)
快?
a[i] = i
只是将值 i
赋值给 a[i]
。这是不是追加,它只是一个简单的assignment。
现在追加:
a = append(a, i)
理论上会发生以下情况:
这会调用内置的 append()
函数。为此,它首先必须复制 a
切片(切片 header,支持数组不是 header 的一部分),并且它必须为可变参数创建一个临时切片将包含值 i
.
然后它必须重新切片 a
如果它有足够的容量(在你的情况下它有)像 a = a[:len(a)+1]
- 这涉及将新切片分配给 a
里面的append()
.
(如果 a
没有足够大的容量来执行追加 "in-place",则必须分配一个新数组,复制切片中的内容,然后assign / append 被执行 - 但这里不是这种情况。)
然后将i
赋值给a[len(a)-1]
。
然后returns来自append()
的新切片,并将这个新切片赋值给局部变量a
.
与简单的作业相比,这里发生了很多事情。即使这些步骤中的许多都经过优化和/或内联,作为将 i
分配给切片元素的最小添加,切片类型的局部变量 a
(这是一个切片header)必须在循环的每个周期中更新。
推荐阅读:The Go Blog: Arrays, slices (and strings): The mechanics of 'append'
自从这个问题发布后,似乎对Go编译器或运行时进行了一些改进,所以现在(Go 1.10.1
)append
和直接通过索引赋值之间没有显着差异.
此外,由于 OOM 恐慌,我不得不稍微更改您的基准测试。
package main
import "testing"
var result []int
const size = 32
const iterations = 100 * 1000 * 1000
func doAssign() {
data := make([]int, size)
for i := 0; i < size; i++ {
data[i] = i
}
result = data
}
func doAppend() {
data := make([]int, 0, size)
for i := 0; i < size; i++ {
data = append(data, i)
}
result = data
}
func BenchmarkAssign(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAssign()
}
}
func BenchmarkAppend(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAppend()
}
}
结果:
➜ bench_slice_assign go test -bench=Bench .
goos: linux
goarch: amd64
BenchmarkAssign-4 100000000 80.9 ns/op
BenchmarkAppend-4 100000000 81.9 ns/op
PASS
ok _/home/isaev/troubles/bench_slice_assign 16.288s
为了使切片追加操作更快,我们需要分配足够的容量。有两种附加切片的方法,代码如下:
func BenchmarkSliceAppend(b *testing.B) {
a := make([]int, 0, b.N)
for i := 0; i < b.N; i++ {
a = append(a, i)
}
}
func BenchmarkSliceSet(b *testing.B) {
a := make([]int, b.N)
for i := 0; i < b.N; i++ {
a[i] = i
}
}
结果是:
BenchmarkSliceAppend-4 200000000 7.87 ns/op 8 B/op 0 allocs/op
BenchmarkSliceSet-4 300000000 5.76 ns/op 8 B/op
为什么 a[i] = i
比 a = append(a, i)
快?
a[i] = i
只是将值 i
赋值给 a[i]
。这是不是追加,它只是一个简单的assignment。
现在追加:
a = append(a, i)
理论上会发生以下情况:
这会调用内置的
append()
函数。为此,它首先必须复制a
切片(切片 header,支持数组不是 header 的一部分),并且它必须为可变参数创建一个临时切片将包含值i
.然后它必须重新切片
a
如果它有足够的容量(在你的情况下它有)像a = a[:len(a)+1]
- 这涉及将新切片分配给a
里面的append()
.
(如果a
没有足够大的容量来执行追加 "in-place",则必须分配一个新数组,复制切片中的内容,然后assign / append 被执行 - 但这里不是这种情况。)然后将
i
赋值给a[len(a)-1]
。然后returns来自
append()
的新切片,并将这个新切片赋值给局部变量a
.
与简单的作业相比,这里发生了很多事情。即使这些步骤中的许多都经过优化和/或内联,作为将 i
分配给切片元素的最小添加,切片类型的局部变量 a
(这是一个切片header)必须在循环的每个周期中更新。
推荐阅读:The Go Blog: Arrays, slices (and strings): The mechanics of 'append'
自从这个问题发布后,似乎对Go编译器或运行时进行了一些改进,所以现在(Go 1.10.1
)append
和直接通过索引赋值之间没有显着差异.
此外,由于 OOM 恐慌,我不得不稍微更改您的基准测试。
package main
import "testing"
var result []int
const size = 32
const iterations = 100 * 1000 * 1000
func doAssign() {
data := make([]int, size)
for i := 0; i < size; i++ {
data[i] = i
}
result = data
}
func doAppend() {
data := make([]int, 0, size)
for i := 0; i < size; i++ {
data = append(data, i)
}
result = data
}
func BenchmarkAssign(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAssign()
}
}
func BenchmarkAppend(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAppend()
}
}
结果:
➜ bench_slice_assign go test -bench=Bench .
goos: linux
goarch: amd64
BenchmarkAssign-4 100000000 80.9 ns/op
BenchmarkAppend-4 100000000 81.9 ns/op
PASS
ok _/home/isaev/troubles/bench_slice_assign 16.288s