数组与切片:访问速度

icz*_*cza 16 arrays performance benchmarking go slice

这个问题是关于访问数组和切片元素的速度,而不是关于将它们作为参数传递给函数的效率.

在大多数情况下,我希望数组切片更快,因为切片是描述数组的连续部分的数据结构,因此在访问切片的元素时可能会涉及额外的步骤(间接地是其底层数组的元素) .

所以我写了一个小测试来测试一批简单的操作.有4个基准函数,前2个测试全局切片和全局数组,另外2个测试局部切片和本地数组:

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我多次运行测试,这是典型的输出(go test -bench .*):

BenchmarkSliceGlobal      300000              4210 ns/op
BenchmarkArrayGlobal      300000              4123 ns/op
BenchmarkSliceLocal       500000              3090 ns/op
BenchmarkArrayLocal       500000              3768 ns/op
Run Code Online (Sandbox Code Playgroud)

分析结果:

访问全局切片比访问全局数组要慢一些,这与我预期的相同:
4210vs 4123ns/op

但访问本地切片要比访问本地阵列快得多:
3090vs 3768ns/op

我的问题是:这是什么原因?

笔记

我尝试改变以下内容,但没有改变结果:

  • 数组/切片的大小(试过100,1000,10000)
  • 基准功能的顺序
  • 阵列/切片的元素类型(试过byteint)

thw*_*hwd 10

比较了AMD64组装两者的BenchmarkArrayLocalBenchmarkSliceLocal(太长,不适合在这个岗位):

阵列版本a多次从内存加载地址,实际上是在每个阵列访问操作上:

LEAQ    "".a+1000(SP),BX
Run Code Online (Sandbox Code Playgroud)

而切片版本在从内存加载一次后专门在寄存器上进行计算:

LEAQ    (DX)(SI*1),BX
Run Code Online (Sandbox Code Playgroud)

这不是决定性的,但可能是原因.原因是两种方法在其他方面几乎完全相同.另一个值得注意的细节是数组版本调用runtime.duffcopy,这是一个相当长的汇编程序,而切片版本则没有.


Pas*_*loe 5

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]bytega[j & 1023].