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

When creating an array in Go, it seems the array will always be zeroed, even if different values will be set right after the initialization, for example when the value should be set to the index in the array.

One way to avoid this is to use array literals, such as a = [5]int{0,1,2,3,4}, but it becomes impractical for long arrays. I'm wondering what is the best way to perform the initialization.

Surprisingly, the named return function outperforms the composite literal initialization for large arrays.

I've created the following benchmark to compare the performance:

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
}

Results:

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

Observations:

  1. I did not expect that naming the return value would make a difference for the loop, I thought surely the compiler would do some optimization. For 1,000,000, it becomes faster than the literal initialization.

  2. I expected a linear scaling, I do not understand why it is not the case, for either of the methods.

I'm not sure how to explain this, even though it seems to be extremely basic. Any ideas ?

Edit: There is an open issue on Github complaining that naming the return value should not make a difference. I also found this to be a surprising behavior.

The reason why your results are not linear with the array size is because not all operations involved in obtaining a new filled array are linear with the array size. For example you need memory allocation, optionally zeroing the allocated memory, a loop to fill the array, and you have to return (copy) the array's memory. Allocation is a good example which should not be linear with the size, also, copying memory should also not be linear (should increase, but not linearly).

One way to avoid a lengthy composite literal and asking a new array value that needs to be zeroed which afterwards gets filled is to have the value ready, and just assign it to the array variable.

What I mean by this is have a package-level variable store the computed / filled array (simplest filled with a simple loop), and when you need a new array filled the same, just assign the stored value:

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

If you add this to your benchmarks:

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

Or simply:

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

This will outperform your fastest-so-far ArrayLoopNamedReturn by twice (when N = 1_000_000).

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

You either initialize the array with literals, as you said, or the array will have default, zero values. If you were able to create an array and set its content later, any read access between those two moments would be undefined (like in C).

I agree that it's not practical to use an array literal for a large number of elements though, but that's the price of memory safety :)