首页 > 解决方案 > 如何在不将所有值归零的情况下初始化一个长 Golang 数组?

问题描述

在 Go 中创建数组时,即使在初始化后立即设置不同的值,例如当值应设置为数组中的索引时,数组似乎也将始终归零。

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

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

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

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. 我没想到命名返回值会对循环产生影响,我认为编译器肯定会做一些优化。对于 1,000,000,它变得比字面初始化更快。

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

我不确定如何解释这一点,即使它似乎非常基本。有任何想法吗 ?

编辑: Github 上有一个未解决的问题,抱怨命名返回值不应该有所作为。我还发现这是一个令人惊讶的行为。

标签: arraysgoinitialization

解决方案


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

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

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

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

推荐阅读