数组 vs 切片:访问速度
Array vs Slice: accessing speed
这个问题是关于访问数组和切片的元素的速度,而不是关于将它们作为参数传递给函数的效率。
在大多数情况下,我希望 arrays 比 slices 更快,因为 slice 是一种描述数组的连续部分的数据结构数组,因此在访问切片的元素(间接访问其底层数组的元素)时可能涉及额外的步骤。
所以我写了一个小测试来对一批简单的操作进行基准测试。有 4 个基准函数,前两个测试一个 global 切片和一个全局数组,另外两个测试一个 local 切片和一个局部数组:
var gs = make([]byte, 1000) // Global slice
var ga [1000]byte // Global array
func BenchmarkSliceGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
for j, v := range gs {
gs[j]++; gs[j] = gs[j] + v + 10; gs[j] += v
}
}
}
func BenchmarkArrayGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
for j, v := range ga {
ga[j]++; ga[j] = ga[j] + v + 10; ga[j] += v
}
}
}
func BenchmarkSliceLocal(b *testing.B) {
var s = make([]byte, 1000)
for i := 0; i < b.N; i++ {
for j, v := range s {
s[j]++; s[j] = s[j] + v + 10; s[j] += v
}
}
}
func BenchmarkArrayLocal(b *testing.B) {
var a [1000]byte
for i := 0; i < b.N; i++ {
for j, v := range a {
a[j]++; a[j] = a[j] + v + 10; a[j] += v
}
}
}
我运行多次测试,这里是典型的输出(go test -bench .*
):
BenchmarkSliceGlobal 300000 4210 ns/op
BenchmarkArrayGlobal 300000 4123 ns/op
BenchmarkSliceLocal 500000 3090 ns/op
BenchmarkArrayLocal 500000 3768 ns/op
分析结果:
访问全局切片比访问全局数组稍慢,这是我预期的:
4210
对比 4123
ns/op
但是访问本地切片比访问本地数组要快得多:
3090
对比 3768
ns/op
我的问题是:这是什么原因?
注释
我尝试改变以下内容,但 none 改变了结果:
- 大小array/slice(试过100、1000、10000)
- 基准函数的顺序
- array/slice 的元素类型(已尝试
byte
和 int
)
比较 BenchmarkArrayLocal
和 BenchmarkSliceLocal
的 the amd64 assembly(太长,不适合这个 post):
数组版本多次从内存中加载 a
的地址,实际上是在每次数组访问操作时:
LEAQ "".a+1000(SP),BX
而切片版本在从内存加载一次后仅在寄存器上计算:
LEAQ (DX)(SI*1),BX
这不是决定性的,但可能是原因。原因是这两种方法在其他方面几乎相同。另一个值得注意的细节是数组版本调用了 runtime.duffcopy
,这是一个相当长的汇编例程,而切片版本则没有。
Go 1.8 版本可以取消一些范围检查,所以差异变大了。
BenchmarkSliceGlobal-4 500000 3220 ns/op
BenchmarkArrayGlobal-4 1000000 1287 ns/op
BenchmarkSliceLocal-4 1000000 1267 ns/op
BenchmarkArrayLocal-4 1000000 1301 ns/op
对于数组,我建议使用 2 的幂的大小并包括逻辑与操作。这样你就可以确定编译器消除了检查。因此 var ga [1024]byte
与 ga[j & 1023]
.
在go1.18和M1上区别更大
我确信数组比切片快,但现在我有证据证明情况并非总是如此
goos: darwin
goarch: arm64
BenchmarkSliceGlobal-8 926196 1257.0 ns/op 0 B/op 0 allocs/op
BenchmarkArrayGlobal-8 2110324 567.0 ns/op 0 B/op 0 allocs/op
BenchmarkSliceLocal-8 2275382 535.0 ns/op 0 B/op 0 allocs/op
BenchmarkArrayLocal-8 1802491 647.4 ns/op 0 B/op 0 allocs/op```
这个问题是关于访问数组和切片的元素的速度,而不是关于将它们作为参数传递给函数的效率。
在大多数情况下,我希望 arrays 比 slices 更快,因为 slice 是一种描述数组的连续部分的数据结构数组,因此在访问切片的元素(间接访问其底层数组的元素)时可能涉及额外的步骤。
所以我写了一个小测试来对一批简单的操作进行基准测试。有 4 个基准函数,前两个测试一个 global 切片和一个全局数组,另外两个测试一个 local 切片和一个局部数组:
var gs = make([]byte, 1000) // Global slice
var ga [1000]byte // Global array
func BenchmarkSliceGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
for j, v := range gs {
gs[j]++; gs[j] = gs[j] + v + 10; gs[j] += v
}
}
}
func BenchmarkArrayGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
for j, v := range ga {
ga[j]++; ga[j] = ga[j] + v + 10; ga[j] += v
}
}
}
func BenchmarkSliceLocal(b *testing.B) {
var s = make([]byte, 1000)
for i := 0; i < b.N; i++ {
for j, v := range s {
s[j]++; s[j] = s[j] + v + 10; s[j] += v
}
}
}
func BenchmarkArrayLocal(b *testing.B) {
var a [1000]byte
for i := 0; i < b.N; i++ {
for j, v := range a {
a[j]++; a[j] = a[j] + v + 10; a[j] += v
}
}
}
我运行多次测试,这里是典型的输出(go test -bench .*
):
BenchmarkSliceGlobal 300000 4210 ns/op
BenchmarkArrayGlobal 300000 4123 ns/op
BenchmarkSliceLocal 500000 3090 ns/op
BenchmarkArrayLocal 500000 3768 ns/op
分析结果:
访问全局切片比访问全局数组稍慢,这是我预期的:
4210
对比 4123
ns/op
但是访问本地切片比访问本地数组要快得多:
3090
对比 3768
ns/op
我的问题是:这是什么原因?
注释
我尝试改变以下内容,但 none 改变了结果:
- 大小array/slice(试过100、1000、10000)
- 基准函数的顺序
- array/slice 的元素类型(已尝试
byte
和int
)
比较 BenchmarkArrayLocal
和 BenchmarkSliceLocal
的 the amd64 assembly(太长,不适合这个 post):
数组版本多次从内存中加载 a
的地址,实际上是在每次数组访问操作时:
LEAQ "".a+1000(SP),BX
而切片版本在从内存加载一次后仅在寄存器上计算:
LEAQ (DX)(SI*1),BX
这不是决定性的,但可能是原因。原因是这两种方法在其他方面几乎相同。另一个值得注意的细节是数组版本调用了 runtime.duffcopy
,这是一个相当长的汇编例程,而切片版本则没有。
Go 1.8 版本可以取消一些范围检查,所以差异变大了。
BenchmarkSliceGlobal-4 500000 3220 ns/op
BenchmarkArrayGlobal-4 1000000 1287 ns/op
BenchmarkSliceLocal-4 1000000 1267 ns/op
BenchmarkArrayLocal-4 1000000 1301 ns/op
对于数组,我建议使用 2 的幂的大小并包括逻辑与操作。这样你就可以确定编译器消除了检查。因此 var ga [1024]byte
与 ga[j & 1023]
.
在go1.18和M1上区别更大 我确信数组比切片快,但现在我有证据证明情况并非总是如此
goos: darwin
goarch: arm64
BenchmarkSliceGlobal-8 926196 1257.0 ns/op 0 B/op 0 allocs/op
BenchmarkArrayGlobal-8 2110324 567.0 ns/op 0 B/op 0 allocs/op
BenchmarkSliceLocal-8 2275382 535.0 ns/op 0 B/op 0 allocs/op
BenchmarkArrayLocal-8 1802491 647.4 ns/op 0 B/op 0 allocs/op```