【问题标题】:Array vs Slice: accessing speedArray vs Slice:访问速度
【发布时间】:2015-08-12 01:27:26
【问题描述】:

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

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

所以我写了一个小测试来对一批简单的操作进行基准测试。有 4 个基准函数,前 2 个测试 global 切片和全局数组,另外 2 个测试 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 vs 4123ns/op

但是访问本地切片比访问本地数组要快得多:
3090 vs 3768ns/op

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

备注

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

  • 数组/切片的大小(已尝试 100、1000、10000)
  • 基准函数的顺序
  • 数组/切片的元素类型(试过byteint

【问题讨论】:

  • 有几个因素会影响这样的微基准:边界检查、分配(堆、堆栈)、代码生成、窥视孔优化等。看看在不同条件下生成的程序集输出,如禁用边界检查或禁用优化。
  • 有趣的是,如果您将pointer to array 添加到基准测试中,您会发现它们的性能与切片大致相同。

标签: arrays performance go benchmarking slice


【解决方案1】:

比较BenchmarkArrayLocalBenchmarkSliceLocal 中的the amd64 assembly(太长,不适合这篇文章):

数组版本从内存中多次加载a的地址,实际上是在每次数组访问操作时:

LEAQ    "".a+1000(SP),BX

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

LEAQ    (DX)(SI*1),BX

这不是决定性的,但可能是原因。原因是这两种方法实际上是相同的。另一个值得注意的细节是数组版本调用了runtime.duffcopy,这是一个相当长的汇编例程,而切片版本没有。

【讨论】:

  • 但是runtime.duffcopy() 只被调用一次对吗?看到基准循环执行了 50 万次 (N),这应该不会对结果产生任何影响。
  • 是的,我也是这么认为的。
  • 在全局数组和切片的情况下,几乎相同的结果是否来自这样一个事实,即在这种情况下编译的代码不包括“注册表”优化并且从内存中加载相同的地址?
  • 是的,完全组装here。两个全局版本都从内存中加载,就像本地数组版本一样。
  • 谢谢,很有帮助。我想知道为什么在循环访问本地数组的情况下不使用寄存器优化...
【解决方案2】:

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 &amp; 1023]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-12-05
    • 2014-01-25
    • 1970-01-01
    • 2014-07-08
    • 1970-01-01
    • 2012-09-25
    • 1970-01-01
    • 2014-12-15
    相关资源
    最近更新 更多