簡體   English   中英

Go 中的垃圾收集和指針的正確使用

[英]Garbage collection and correct usage of pointers in Go

我來自 Python/Ruby/JavaScript 背景。 我了解指針的工作原理,但是,我不完全確定如何在以下情況下利用它們。

假設我們有一個虛構的 Web API,它搜索某個圖像數據庫並返回一個 JSON,描述找到的每個圖像中顯示的內容:

[
    {
        "url": "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
        "description": "Ocean islands",
        "tags": [
            {"name":"ocean", "rank":1},
            {"name":"water", "rank":2},
            {"name":"blue", "rank":3},
            {"name":"forest", "rank":4}
        ]
    },

    ...

    {
        "url": "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
        "description": "Bridge over river",
        "tags": [
            {"name":"bridge", "rank":1},
            {"name":"river", "rank":2},
            {"name":"water", "rank":3},
            {"name":"forest", "rank":4}
        ]
    }
]

我的目標是在 Go 中創建一個數據結構,將每個標簽映射到一個圖像 URL 列表,如下所示:

{
    "ocean": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
    ],
    "water": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "blue": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
    ],
    "forest":[
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg", 
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "bridge": [
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "river":[
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ]
}

如您所見,每個圖像 URL 可以同時屬於多個標簽。 如果我有數以千計的圖像和更多的標簽,如果按每個標簽的值復制圖像 URL 字符串,則此數據結構會變得非常大。 這是我想利用指針的地方。

我可以用 Go 中的兩個結構來表示 JSON API 響應, func searchImages()模仿了假 API:

package main

import "fmt"


type Image struct {
    URL string
    Description string
    Tags []*Tag
}

type Tag struct {
    Name string
    Rank int
}

// this function mimics json.NewDecoder(resp.Body).Decode(&parsedJSON)
func searchImages() []*Image {
    parsedJSON := []*Image{
        &Image {
            URL: "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
            Description: "Ocean islands",
            Tags: []*Tag{
                &Tag{"ocean", 1},
                &Tag{"water", 2},
                &Tag{"blue", 3},
                &Tag{"forest", 4},
            }, 
        },
        &Image {
            URL: "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
            Description: "Bridge over river",
            Tags: []*Tag{
                &Tag{"bridge", 1},
                &Tag{"river", 2},
                &Tag{"water", 3},
                &Tag{"forest", 4},
            }, 
        },
    }
    return parsedJSON
}

現在,導致內存中數據結構非常大的不太理想的映射函數可能如下所示:

func main() {
    result := searchImages()

    tagToUrlMap := make(map[string][]string)

    for _, image := range result {
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], image.URL)
        }
    }

    fmt.Println(tagToUrlMap)
}

我可以修改它以使用指向Image struct URL字段的指針,而不是按值復制它:

    // Version 1

    tagToUrlMap := make(map[string][]*string)

    for _, image := range result {
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &image.URL)
        }
    }

它有效,我的第一個問題是在我以這種方式構建映射后result數據結構會發生什么? Image URL字符串字段是否會以某種方式留在內存中,而其余的result將被垃圾收集? 或者result數據結構會一直留在內存中直到程序結束,因為某些東西指向它的成員?

另一種方法是將 URL 復制到中間變量並使用指向它的指針:

    // Version 2

    tagToUrlMap := make(map[string][]*string)

    for _, image := range result {
        imageUrl = image.URL
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)    
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &imageUrl)
        }
    }

這是否更好? result數據結構會被正確垃圾回收嗎?

或者我應該在Image結構中使用指向字符串的指針?

type Image struct {
    URL *string
    Description string
    Tags []*Tag
}

有一個更好的方法嗎? 我也很欣賞 Go 上任何深入描述指針的各種用途的資源。 謝謝!

https://play.golang.org/p/VcKWUYLIpH7

更新:我擔心最佳內存消耗而不是最生成不需要的垃圾。 我的目標是盡可能使用最少的內存。

前言:我在我的github.com/icza/gox庫中發布了呈現的字符串池,參見stringsx.Pool


首先介紹一下背景。 Go 中的string值由類似結構的小型數據結構reflect.StringHeader

type StringHeader struct {
        Data uintptr
        Len  int
}

所以基本上傳遞/復制一個string值傳遞/復制這個小的結構值,無論string的長度如何,它都是2個字。 在 64 位體系結構上,即使string有一千個字符,它也只有 16 個字節。

所以基本上string值已經充當了指針。 引入另一個像*string這樣的指針只會使使用復雜化,並且您不會真正獲得任何顯着的內存。 為了內存優化,忘記使用*string

它有效,我的第一個問題是在我以這種方式構建映射后結果數據結構會發生什么? 圖像 URL 字符串字段是否會以某種方式留在內存中,而其余的結果將被垃圾收集? 或者結果數據結構會一直留在內存中直到程序結束,因為某些東西指向它的成員?

如果你有一個指針值指向一個結構體值的一個字段,那么整個結構體將被保存在內存中,它不能被垃圾回收。 請注意,雖然可以釋放為結構體的其他字段保留的內存,但當前的 Go 運行時和垃圾收集器不會這樣做。 因此,為了實現最佳內存使用,您應該忘記存儲結構字段的地址(除非您還需要完整的結構值,但仍然需要小心存儲字段地址和切片/數組元素地址)。

這樣做的原因是因為 struct 值的內存被分配為一個連續的段,因此只保留一個引用字段會強烈地分割可用/空閑內存,並使最佳內存管理更加困難和效率低下。 對這些區域進行碎片整理還需要復制引用字段的內存區域,這將需要“實時更改”指針值(更改內存地址)。

因此,雖然使用指向string值的指針可能會為您節省一些很小的內存,但增加的復雜性和額外的間接性使其不值得。

那該怎么辦呢?

“最優”解決方案

所以最干凈的方法是繼續使用string值。

還有一個我們之前沒有提到的優化。

您可以通過解組 JSON API 響應來獲得結果。 這意味着如果在 JSON 響應中多次包含相同的 URL 或標記值,將為它們創建不同的string值。

這是什么意思? 如果您在 JSON 響應中有兩次相同的 URL,在解組后,您將有 2 個不同的string值,其中將包含 2 個不同的指針,指向 2 個不同的已分配字節序列(否則字符串內容將相同)。 encoding/json包不做string實習

這是一個證明這一點的小應用程序:

var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
    panic(err)
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data)
}

上面的輸出(在Go Playground上試試):

273760312
273760315
273760320

我們看到 3 個不同的指針。 它們可能相同,因為string值是不可變的。

json包不會檢測重復的string值,因為檢測會增加內存和計算開銷,這顯然是不需要的。 但在我們的例子中,我們追求最佳內存使用,因此“初始”額外計算確實值得大內存增益。

所以讓我們做我們自己的字符串實習。 怎么做?

解組 JSON 結果后,在構建tagToUrlMap映射期間,讓我們跟蹤我們遇到的string值,如果之前已經看到后續string值,只需使用該早期值(其字符串描述符)。

這是一個非常簡單的字符串內部實現:

var cache = map[string]string{}

func interned(s string) string {
    if s2, ok := cache[s]; ok {
        return s2
    }
    // New string, store it
    cache[s] = s
    return s
}

讓我們在上面的示例代碼中測試這個“內部人員”:

var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
    panic(err)
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data, s[i])
}

for i := range s {
    s[i] = interned(s[i])
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data, s[i])
}

上面的輸出(在Go Playground上試試):

273760312 abc
273760315 abc
273760320 abc
273760312 abc
273760312 abc
273760312 abc

精彩的! 正如我們所見,在使用我們的interned()函數之后,我們的數據結構中只使用了"abc"字符串的一個實例(實際上是第一次出現)。 這意味着所有其他實例(假設沒有其他人使用它們)可以並且將被正確垃圾收集(由垃圾收集器,在未來的某個時間)。

這里不要忘記的一件事是:字符串內部使用一個cache字典來存儲所有以前遇到的字符串值。 所以為了讓這些字符串消失,你也應該“清除”這個緩存映射,最簡單的方法是為其分配一個nil值。

事不宜遲,讓我們看看我們的解決方案:

result := searchImages()

tagToUrlMap := make(map[string][]string)

for _, image := range result {
    imageURL := interned(image.URL)

    for _, tag := range image.Tags {
        tagName := interned(tag.Name)
        tagToUrlMap[tagName] = append(tagToUrlMap[tagName], imageURL)
    }
}

// Clear the interner cache:
cache = nil

要驗證結果:

enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
if err := enc.Encode(tagToUrlMap); err != nil {
    panic(err)
}

輸出是(在Go Playground上試試):

{
  "blue": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
  ],
  "bridge": [
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "forest": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "ocean": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
  ],
  "river": [
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "water": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ]
}

進一步的內存優化:

我們使用內置的append()函數向標簽添加新的圖像 URL。 append()可能(並且通常確實)分配比需要更大的切片(考慮未來的增長)。 在我們的“構建”過程之后,我們可以通過我們的tagToUrlMap映射並將這些切片“修剪”到所需的最小值。

這是如何做到的:

for tagName, urls := range tagToUrlMap {
    if cap(urls) > len(urls) {
        urls2 := make([]string, len(urls))
        copy(urls2, urls)
        tagToUrlMap[tagName] = urls2
    }
}

[...] 會被正確垃圾回收嗎?

是的。

您永遠不必擔心會收集仍在使用的東西,一旦不再使用,您就可以依靠收集的所有東西。

所以關於 GC 的問題永遠不是“它會被正確收集嗎?” 但是“我會產生不必要的垃圾嗎?”。 現在這個實際問題並不取決於數據結構,而是取決於創建的 neu 對象的數量(在堆上)。 所以這是一個關於如何使用數據結構的問題,而不是關於結構本身的問題。 使用基准測試並使用 -benchmem 運行 go test。

(高端性能可能還會考慮 GC 需要做多少工作:掃描指針可能需要時間。暫時忘記這一點。)

另一個相關問題是關於內存消耗 復制字符串只復制三個單詞,而復制 *string 復制一個單詞。 因此,這里使用 *string 並沒有什么安全措施。

所以不幸的是,相關問題(產生的垃圾量和總內存消耗)沒有明確的答案。 不要過度考慮問題,使用適合您的目的,衡量和重構。

暫無
暫無

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

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