简体   繁体   English

如何在 Go 中清除和重用数组(不是切片)?

[英]How to clear and reuse an array (not slice) in Go?

How can I clear and reuse an array in Go?如何在 Go 中清除和重用数组? Is manual for loop with assigning all values to their default the only solution?将所有值分配给默认值的手动 for 循环是唯一的解决方案吗?

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1,2,3}
    fmt.Println(arr) // Output: [1 2 3]

    // clearing an array - is there a faster/easier/less verbose way?
    for i := range arr {
        arr[i] = 0
    }
    
    fmt.Println(arr) // Output: [0 0 0]
}

Given that the variable arr is already instantiated as type [3]int , I can remember a few options to override its content:鉴于变量arr已经实例化为类型[3]int ,我可以记住一些覆盖其内容的选项:

    arr = [3]int{}

or或者

    arr = make([]int, 3)

Both of them, overrides the slice with values of 0 's.它们都用0的值覆盖切片。

Keep in mind that, everytime we use this syntax var := Type{} instantiates a new object of the given Type to the variable.请记住,每次我们使用这种语法时var := Type{}将给定 Type 的新对象实例化为变量。 So, if you get the same variable and instantiates it again, you will be overriding it's content to a new one.所以,如果你得到相同的变量并再次实例化它,你将把它的内容覆盖到一个新的。

In Go, the language treats a slice like a type object, and not a primitive type like int , rune , byte , etc.在 Go 中,语言将切片视为类型对象,而不是像intrunebyte等原始类型。

I see from your comment that you are still not sure what is going on here:我从你的评论中看到你仍然不确定这里发生了什么:

I want to allocate an array once and then reuse it (clearing before use, of course) in iterations of a for loop.我想分配一个数组一次,然后在 for 循环的迭代中重用它(当然在使用前清除)。 Do you mean that arr = [3]int{} does reallocation as opposed to clearing with a for i := range arr {arr[i] = 0} ?你的意思是arr = [3]int{}重新分配而不是用 a for i := range arr {arr[i] = 0}清除?

First, let's toss arrays entirely out of the question.首先,让我们完全排除数组的问题。 Suppose we have this loop:假设我们有这个循环:

for i := 0; i < 10; i++ {
    v := 3
    // ... code that uses v, but never shows &v
}

Does this create a new variable v on each trip through the loop, or does this create one variable v outside the loop and on each trip through the loop, stick 3 into the variable at the top of the loop?这是否会在每次循环时创建一个变量v ,或者这是否会在循环外创建一个变量v ,并且在每次循环时,将3粘贴到循环顶部的变量中? The language proper does not have an answer to this question for you.正确语言无法为您解答这个问题。 The language describes the behavior of the program, which is that v is initialized to 3 each time, but if we never observe &v , perhaps &v is the same each time.该语言描述了程序的行为,即v每次初始化为 3,但如果我们从未观察到&v ,也许&v每次都是相同的

If we choose to observe &v , and actually run this program in some implementation, we'll see the implementation's answer .如果我们选择观察&v ,并在某些实现中实际运行该程序,我们将看到实现的答案 Now things get interesting.现在事情变得有趣了。 The language says that each v —each one allocated in a new trip through the loop—is independent of any previous v .语言表示,每个 v——在循环的新行程中分配的每个v独立于任何先前的v If we take &v , we have at least a potential ability for each instance of v to still be live on a subsequent trip through the loop.如果我们采用&v ,我们至少有一个潜在的能力让v每个实例在随后的循环中仍然存在。 So each v must not interfere with any previous variable v .因此,每个v不得干扰任何先前的变量v The easy way for the compiler to guarantee this to allocate it anew each time, using reallocation.编译器使用重新分配来保证每次重新分配它的简单方法。

The current Go compilers use escape analysis to try to detect whether some variable's address is taken.当前的 Go 编译器使用转义分析来尝试检测是否使用了某个变量的地址。 If so, that variable is heap-allocated rather than stack-allocated, and the runtime system relies on the (runtime) garbage collector to free the variable.如果是这样,该变量是堆分配的而不是堆栈分配的,并且运行时系统依赖(运行时)垃圾收集器来释放变量。 We can demonstrate this with a simple program on the Go playground :我们可以在 Go playground 上用一个简单的程序来演示这一点:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    for i := 0; i < 10; i++ {
        if i > 5 {
            runtime.GC()
        }
        v := 3
        fmt.Printf("v is at %p\n", &v)
    }
}

This program's output is not guaranteed to look like this, but here is what I get when running it:这个程序的输出不能保证看起来像这样,但这是我运行它时得到的:

v is at 0xc00002c008
v is at 0xc00002c048
v is at 0xc00002c050
v is at 0xc00002c058
v is at 0xc00002c060
v is at 0xc00002c068
v is at 0xc000094040
v is at 0xc000094050
v is at 0xc000094040
v is at 0xc000094050

Note how v 's addresses start coinciding (albeit alternating) once we force the garbage collector to start running, when i takes on values 6 through 10. That's because v really does get allocated anew each time, but by having the GC run, we make some previously-allocated, no-longer-in-use memory available again.请注意v的地址如何在我们强制垃圾收集器开始运行时开始重合(尽管交替),当i取值 6 到 10 时。那是因为v确实每次都重新分配,但是通过运行 GC,我们使一些先前分配的不再使用的内存再次可用。 (Precisely why this alternates is a bit of a mystery, but the behavior is likely to depend on many factors, such as the Go version, runtime startup allocations, how many threads your system is willing to use, and so on.) (确切地说,这种交替的原因有点神秘,但这种行为可能取决于许多因素,例如 Go 版本、运行时启动分配、系统愿意使用多少线程等等。)

What we've shown here is that Go's escape analysis thinks that v escaped, so it allocates a new one each time.我们在这里展示的是 Go 的逃逸分析认为v逃逸了,所以它每次都分配一个新的。 We passed &v to fmt.Printf , which is what let it escape.我们将&v传递给fmt.Printf ,这就是让它逃脱的原因。 A future compiler might be smarter: it might know that fmt.Printf does not save the value of &v , so that the variable is dead after fmt.Printf returns, and did not actually escape;未来的编译器可能会更聪明:它可能知道fmt.Printf保存&v的值,因此在fmt.Printf返回后变量就死了,并且实际上并没有逃逸; in that case, it might re-use &v each time.在这种情况下,它可能每次都重复使用&v But as soon as we add something where that would matter in an important way, v really would escape and the compiler would have to go back to allocating each one separately.但是一旦我们在重要的地方添加了一些东西, v真的逃逸,编译器将不得不回到单独分配每个。

The key question is observability关键问题是可观察性

Unless you take the address of a variable—your entire array or any of its elements, for instance—in Go, the only thing you can observe about the thing is its type and value.除非你把可变整个数组的地址或任何元素,例如,在围棋,你可以观察到有关的事情的唯一的事情是它的类型和价值。 In general, means you can't tell if the compiler has made a new copy of some variable, or reused an old copy.一般而言,这意味着您无法判断编译器是否制作了某个变量的新副本,或重用了旧副本。

If you pass an array to a function, Go passes the entire array by value .如果将数组传递给函数,Go 会按值传递整个数组。 This means that the function cannot change the original values in the array.这意味着该函数无法更改数组中的原始值。 We can see how this is observable by writing a function that does in fact change the values:我们可以通过编写一个实际上改变值的函数来了解这是如何观察到的:

package main

import (
    "fmt"
)

func observe(arr [3]int) {
    fmt.Printf("at start: arr = %v\n", arr)
    for i, v := range arr {
        arr[i] = v * 2
    }
    fmt.Printf("at end: arr = %v\n", arr)
}

func main() {
    a := [3]int{1, 2, 3}
    for i := 0; i < 3; i++ {
        observe(a)
    }
}

( playground link ). 游乐场链接)。

Arrays in Go are passed by value, so this does not change the array a in main even though it does change the array arr in observe . Go 中的数组是按值传递的,因此这不会更改main的数组a即使它确实更改了observe的数组arr

Often, though, we'd like to change the array and preserve these changes.不过,通常来说,我们改变数组并保存这些变化。 To do that, we must either:为此,我们必须:

Now we can see the values change, with those changes preserved across function calls, even if we never look at the various addresses.现在我们可以看到值发生了变化,这些变化在函数调用中得以保留,即使我们从未查看过各种地址。 The language says that we must be able to see these changes, so we can;语言说我们必须能够看到这些变化,所以我们可以; the language says that we must not be able to see the changes when we pass the array itself by value, so we can't.语言说当我们按值传递数组本身时,我们一定不能看到变化,所以我们不能。 It's up to the compiler to come up with some way of making this happen.这取决于编译器想出某种方法来实现这一点。 Whether that involves copying the original array, or some other mystery magic, is up to the compiler—though Go tries to be a straightforward language where the "magic" is obvious, and the obvious way is to copy, or not copy, as appropriate.无论是复制原始数组,还是其他一些神秘的魔法,都取决于编译器——尽管 Go 试图成为一种直截了当的语言,其中“魔法”是显而易见的,显而易见的方法是复制或不复制,视情况而定.

The point of all this这一切的重点

Besides worrying about observable effects—ie, whether we're computing the right answer in the first place—the point of doing all of these experiments is to show that the compiler could do whatever it wants to, as long as it produces the right observable effects.除了担心可观察到的影响——即,我们是否首先计算了正确的答案——做所有这些实验的目的是表明编译器可以做任何它想做的事,只要它产生正确的可观察性效果。

You can attempt to make things easier for the compiler, eg, by allocating a single array, using it by address ( &a in the example above) or slice ( a[:] in the example above), and clearing it yourself.您可以尝试使编译器更容易,例如,通过分配单个数组,按地址(上面示例中的&a )或切片(上面示例中的a[:] )使用它,并自己清除它。 1 But this might not be any faster, and might even be slower , than just writing it however you find clearest. 1但是,这可能不会更快,甚至可能比仅以您认为最清晰的方式编写它更慢 Write it the clear way first, then time it.先写清楚,然后计时。 If it's too slow, try assisting the compiler, and time it again.如果它太慢,请尝试协助编译器,然后再次计时。 Your assist might make things worse or have no effect: if so, don't bother.您的协助可能会使事情变得更糟或没有效果:如果是这样,请不要打扰。 If it makes things much better, keep it.如果它使事情变得更好,请保留它。


1 Knowing that the compiler you're using does escape analysis, if you want to help it out, you can run it with the flags that make it tell you which variables have escaped. 1知道您正在使用的编译器进行转义分析,如果您想帮助它,您可以使用标志运行它,使其告诉您哪些变量已转义。 These are often opportunities for optimization: if you can figure out a way to keep the variable from escaping, the compiler can allocate it on the stack, rather than the heap, which may save a useful amount of time.这些通常是优化的机会:如果你能找到一种方法来防止变量逃逸,编译器可以在堆栈上而不是在堆上分配它,这可能会节省大量的时间。 But if your time isn't really being spent in the allocator in the first place, this won't actually help anyway, so profiling is usually the first step.但是,如果你的时间是不是真的被消耗在分配器摆在首位,这实际上并不会帮助无论如何,所以剖析通常是第一步

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM