簡體   English   中英

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

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

如何在 Go 中清除和重用數組? 將所有值分配給默認值的手動 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]
}

鑒於變量arr已經實例化為類型[3]int ,我可以記住一些覆蓋其內容的選項:

    arr = [3]int{}

或者

    arr = make([]int, 3)

它們都用0的值覆蓋切片。

請記住,每次我們使用這種語法時var := Type{}將給定 Type 的新對象實例化為變量。 所以,如果你得到相同的變量並再次實例化它,你將把它的內容覆蓋到一個新的。

在 Go 中,語言將切片視為類型對象,而不是像intrunebyte等原始類型。

我從你的評論中看到你仍然不確定這里發生了什么:

我想分配一個數組一次,然后在 for 循環的迭代中重用它(當然在使用前清除)。 你的意思是arr = [3]int{}重新分配而不是用 a for i := range arr {arr[i] = 0}清除?

首先,讓我們完全排除數組的問題。 假設我們有這個循環:

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

這是否會在每次循環時創建一個變量v ,或者這是否會在循環外創建一個變量v ,並且在每次循環時,將3粘貼到循環頂部的變量中? 正確語言無法為您解答這個問題。 該語言描述了程序的行為,即v每次初始化為 3,但如果我們從未觀察到&v ,也許&v每次都是相同的

如果我們選擇觀察&v ,並在某些實現中實際運行該程序,我們將看到實現的答案 現在事情變得有趣了。 語言表示,每個 v——在循環的新行程中分配的每個v獨立於任何先前的v 如果我們采用&v ,我們至少有一個潛在的能力讓v每個實例在隨后的循環中仍然存在。 因此,每個v不得干擾任何先前的變量v 編譯器使用重新分配來保證每次重新分配它的簡單方法。

當前的 Go 編譯器使用轉義分析來嘗試檢測是否使用了某個變量的地址。 如果是這樣,該變量是堆分配的而不是堆棧分配的,並且運行時系統依賴(運行時)垃圾收集器來釋放變量。 我們可以在 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)
    }
}

這個程序的輸出不能保證看起來像這樣,但這是我運行它時得到的:

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

請注意v的地址如何在我們強制垃圾收集器開始運行時開始重合(盡管交替),當i取值 6 到 10 時。那是因為v確實每次都重新分配,但是通過運行 GC,我們使一些先前分配的不再使用的內存再次可用。 (確切地說,這種交替的原因有點神秘,但這種行為可能取決於許多因素,例如 Go 版本、運行時啟動分配、系統願意使用多少線程等等。)

我們在這里展示的是 Go 的逃逸分析認為v逃逸了,所以它每次都分配一個新的。 我們將&v傳遞給fmt.Printf ,這就是讓它逃脫的原因。 未來的編譯器可能會更聰明:它可能知道fmt.Printf保存&v的值,因此在fmt.Printf返回后變量就死了,並且實際上並沒有逃逸; 在這種情況下,它可能每次都重復使用&v 但是一旦我們在重要的地方添加了一些東西, v真的逃逸,編譯器將不得不回到單獨分配每個。

關鍵問題是可觀察性

除非你把可變整個數組的地址或任何元素,例如,在圍棋,你可以觀察到有關的事情的唯一的事情是它的類型和價值。 一般而言,這意味着您無法判斷編譯器是否制作了某個變量的新副本,或重用了舊副本。

如果將數組傳遞給函數,Go 會按值傳遞整個數組。 這意味着該函數無法更改數組中的原始值。 我們可以通過編寫一個實際上改變值的函數來了解這是如何觀察到的:

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)
    }
}

游樂場鏈接)。

Go 中的數組是按值傳遞的,因此這不會更改main的數組a即使它確實更改了observe的數組arr

不過,通常來說,我們改變數組並保存這些變化。 為此,我們必須:

現在我們可以看到值發生了變化,這些變化在函數調用中得以保留,即使我們從未查看過各種地址。 語言說我們必須能夠看到這些變化,所以我們可以; 語言說當我們按值傳遞數組本身時,我們一定不能看到變化,所以我們不能。 這取決於編譯器想出某種方法來實現這一點。 無論是復制原始數組,還是其他一些神秘的魔法,都取決於編譯器——盡管 Go 試圖成為一種直截了當的語言,其中“魔法”是顯而易見的,顯而易見的方法是復制或不復制,視情況而定.

這一切的重點

除了擔心可觀察到的影響——即,我們是否首先計算了正確的答案——做所有這些實驗的目的是表明編譯器可以做任何它想做的事,只要它產生正確的可觀察性效果。

您可以嘗試使編譯器更容易,例如,通過分配單個數組,按地址(上面示例中的&a )或切片(上面示例中的a[:] )使用它,並自己清除它。 1但是,這可能不會更快,甚至可能比僅以您認為最清晰的方式編寫它更慢 先寫清楚,然后計時。 如果它太慢,請嘗試協助編譯器,然后再次計時。 您的協助可能會使事情變得更糟或沒有效果:如果是這樣,請不要打擾。 如果它使事情變得更好,請保留它。


1知道您正在使用的編譯器進行轉義分析,如果您想幫助它,您可以使用標志運行它,使其告訴您哪些變量已轉義。 這些通常是優化的機會:如果你能找到一種方法來防止變量逃逸,編譯器可以在堆棧上而不是在堆上分配它,這可能會節省大量的時間。 但是,如果你的時間是不是真的被消耗在分配器擺在首位,這實際上並不會幫助無論如何,所以剖析通常是第一步

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM