【问题标题】:Golang slice append vs assign performanceGolang 切片追加与分配性能
【发布时间】:2016-12-03 22:18:46
【问题描述】:

为了使切片追加操作更快,我们需要分配足够的容量。追加切片有两种方式,代码如下:

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] = ia = append(a, i) 快?

【问题讨论】:

  • 很高兴知道经典的按索引赋值更快。我认为append 方式很奇怪且容易出错。 ????‍♂️

标签: performance go slice


【解决方案1】:

自从发布此问题以来,似乎已经引入了 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

【讨论】:

  • 这是个好消息,但请记住,如果 size 更大并且未指定初始阵列容量,情况就会大不相同。
  • @ParthMehrotra 有什么不同?你能说说你的看法吗?谢谢。
  • 调整数组大小是一项代价高昂的操作。如果您不知道数组的初始大小,那么这些重新分配将使整个操作花费更长的时间。
  • 这个基准对于它所测量的内容令人怀疑。我认为它衡量的是分配成本,而不是两种不同模式的速度比较。
  • 无法在 go1.16.4 上以 100K size 重现您的结果。
【解决方案2】:

a[i] = i 只是将值i 分配给a[i]。这不是附加的,只是一个简单的assignment

现在追加:

a = append(a, i)

理论上会发生以下情况:

  1. 这会调用内置的append() 函数。为此,它首先必须复制a 切片(切片头,后备数组不是头的一部分),并且它必须为可变参数创建一个临时切片,其中包含值i

  2. 然后它必须对 a 进行重新切片,如果它有足够的容量(在你的情况下),如 a = a[:len(a)+1] - 这涉及将新切片分配给 aappend() 内。
    (如果a 没有足够大的容量来“就地”进行追加,则必须分配一个新数组,复制切片中的内容,然后执行分配/追加 -但这里不是这样。)

  3. 然后将i分配给a[len(a)-1]

  4. 然后从append()返回新切片,并将这个新切片赋值给局部变量a

与简单的分配相比,这里发生了很多事情。即使这些步骤中的许多已被优化和/或内联,作为将i 分配给切片元素的最低限度,切片类型的局部变量a(这是一个切片header) 必须在循环的每个循环中更新

推荐阅读:The Go Blog: Arrays, slices (and strings): The mechanics of 'append'

【讨论】:

  • 有没有办法在不重新分配的情况下附加到切片?
  • 鉴于 append 做了很多事情,是否值得执行 copy(a,b) 后跟 if len(a)&lt;len(b){a = append(a, b[len(a):]...)}
  • @mh-cbon 这真的取决于性能有多重要。 append() 可能更清晰易读,这也很重要。如果每一纳秒都很重要,那么也许。如果选择了可读性较差的版本,则应进行衡量并正确记录。
猜你喜欢
  • 2016-07-07
  • 2018-04-15
  • 2017-06-17
  • 2016-02-20
  • 1970-01-01
  • 2015-09-15
  • 1970-01-01
  • 1970-01-01
  • 2016-12-06
相关资源
最近更新 更多