如何在不将所有值清零的情况下初始化长 Golang 数组?

How to initialize a long Golang array without zeroing all the values?

在 Go 中创建数组时,似乎数组总是会被清零,即使在初始化后立即设置不同的值,例如当值应该设置为数组中的索引时。

避免这种情况的一种方法是使用数组文字,例如 a = [5]int{0,1,2,3,4},但对于长数组来说这变得不切实际。我想知道执行初始化的最佳方法是什么。

令人惊讶的是,命名的 return 函数优于大型数组的复合文字初始化。

我创建了以下基准来比较性能:

package main

import "testing"

const N = 1000000

var result [N]int

func arrayLiteral() [N]int {
    // Replace the 3 dots with the actual value
    // I copy-pasted the output of an other program to do this
    return [N]int{0,1,2,3,...,N-1}
}

func arrayLoopNamedReturn() (a [N]int) {
    for i := 0; i < N; i++ {
        a[i] = i
    }
    return
}

func arrayLoop() [N]int {
    var a [N]int
    for i := 0; i < N; i++ {
        a[i] = i
    }
    return a
}

func BenchmarkArrayLoop(b *testing.B) {
    var r [N]int
    for n := 0; n < b.N; n++ {
        r = arrayLoop()
    }
    result = r
}

func BenchmarkArrayLoopNamedReturn(b *testing.B) {
    var r [N]int
    for n := 0; n < b.N; n++ {
        r = arrayLoopNamedReturn()
    }
    result = r
}

func BenchmarkArrayLiteral(b *testing.B) {
    var r [N]int
    for n := 0; n < b.N; n++ {
        r = arrayLiteral()
    }
    result = r
}

结果:

N = 10,000
BenchmarkArrayLoop-8                      200000              9041 ns/op
BenchmarkArrayLoopNamedReturn-8           200000              6327 ns/op
BenchmarkArrayLiteral-8                   300000              4300 ns/op

N = 100,000
BenchmarkArrayLoop-8                       10000            191582 ns/op
BenchmarkArrayLoopNamedReturn-8            20000             76125 ns/op
BenchmarkArrayLiteral-8                    20000             62714 ns/op

N = 1,000,000
BenchmarkArrayLoop-8                         500           2635713 ns/op
BenchmarkArrayLoopNamedReturn-8             1000           1537282 ns/op
BenchmarkArrayLiteral-8                     1000           1854348 ns/op

观察:

  1. 我没想到命名return值会对循环产生影响,我认为编译器肯定会做一些优化。对于 1,000,000,它变得比文字初始化更快。

  2. 我期望线性缩放,我不明白为什么这两种方法都不是这种情况。

我不确定如何解释这一点,尽管它看起来非常基础。有什么想法吗?

编辑:open issue on Github 抱怨命名 return 值应该没有什么不同。我也发现这是一个令人惊讶的行为。

您要么像您所说的那样用文字初始化数组,要么数组将具有默认的零值。如果您能够创建一个数组并稍后设置其内容,那么这两个时刻之间的任何读取访问都将是未定义的(就像在 C 中一样)。

我同意对大量元素使用数组文字是不切实际的,但这是内存安全的代价:)

您的结果与数组大小不成线性关系的原因是,并非所有涉及获取新填充数组的操作都与数组大小成线性关系。例如,您需要内存分配,可以选择将分配的内存归零,循环填充数组,并且您必须 return (复制)数组的内存。分配是一个很好的例子,它不应该与大小成线性关系,同样,复制内存也不应该是线性的(应该增加,但不是线性的)。

避免冗长的复合文字并询问需要归零并随后填充的新数组值的一种方法是准备好值,然后将其分配给数组变量。

我的意思是有一个包级变量存储计算/填充的数组(最简单的填充一个简单的循环),当你需要一个新的数组填充相同的时候,只需分配存储的值:

var cache [N]int

func init() {
    for i := range cache {
        cache[i] = i
    }
}

// If you now need a new array:
var result = cache
// Or re-init an existing array:
result = cache

如果将此添加到基准测试中:

func BenchmarkArrayAssign(b *testing.B) {
    var r [N]int
    for n := 0; n < b.N; n++ {
        r = cache
    }
    result = r
}

或者简单地说:

func BenchmarkArrayAssign(b *testing.B) {
    for n := 0; n < b.N; n++ {
        result = cache
    }
}

这将比您迄今为止最快的ArrayLoopNamedReturn快两倍(当N = 1_000_000)。

BenchmarkArrayAssign-4                  1000       1104829 ns/op
BenchmarkArrayLoop-4                     500       3822005 ns/op
BenchmarkArrayLoopNamedReturn-4          500       2326498 ns/op