当前位置: 首页 > 面试题库 >

垃圾回收和Go中指针的正确使用

松波
2023-03-14
问题内容

我来自Python / Ruby / JavaScript背景。我了解指针的工作原理,但是,我不确定在以下情况下如何利用指针。

假设我们有一个虚构的Web API,该API搜索一些图像数据库并返回一个JSON,该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)
}

我可以修改它以使用指向Imagestruct 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

首先介绍一些背景。stringGo中的值由类似struct的小型数据结构表示reflect.StringHeader

type StringHeader struct {
        Data uintptr
        Len  int
}

因此,基本上传递/复制一个string值会传递/复制这个小的struct值,无论的长度如何,它都是2个字string。在64位体系结构上,即使只有string1000个字符,也只有16个字节。

因此,基本上,string值已充当指针。引入另一个指针*string只会使用法变得复杂,并且您实际上不会获得任何显着的内存。为了优化内存,请不要使用*string

它起作用了,我的第一个问题是,以这种方式构建映射后,结果数据结构会怎样?图像URL字符串字段是否会以某种方式保留在内存中,其余结果将被垃圾回收?还是结果数据结构会保留在内存中直到程序结束,因为某些内容指向其成员?

如果您有一个指向结构值字段的指针值,则整个结构将保存在内存中,无法进行垃圾回收。请注意,虽然可以释放为该结构的其他字段保留的内存,但是当前的Go运行时和垃圾回收器不会这样做。因此,要获得最佳的内存使用率,您应该忘记存储结构字段的地址(除非您还需要完整的结构值,但是仍然需要特别注意存储字段地址和切片/数组元素地址)。

这样做的原因是因为用于结构值的内存被分配为连续的段,因此仅保留一个引用的字段将严重破坏可用/可用内存,并使最佳内存管理更加困难和效率降低。对这些区域进行碎片整理还需要复制参考字段的存储区域,这将需要“实时更改”指针值(更改存储地址)。

因此,尽管使用指向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
}

让我们在上面的示例代码中测试这个“ interner”:

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


 类似资料:
  • Go语言自带垃圾回收机制(GC)。GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源。 GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC() 函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用 runtime.GC() ,这样会立即释放一大片内存,但是会造成程序短时间的性能下降

  • 垃圾回收 我们对生产中花了很多时间来调整垃圾回收。垃圾回收的关注点与Java大致相似,尽管一些惯用的Scala代码比起惯用的Java代码会容易产生更多(短暂的)垃圾——函数式风格的副产品。Hotspot的分代垃圾收集通常使这不成问题,因为短暂的(short-lived)垃圾在大多情形下会被有效的释放掉。 在谈GC调优话题前,先看看这个Attila的报告,它阐述了我们在GC方面的一些经验。 Scal

  • 对于开发者来说,JavaScript 的内存管理是自动的、无形的。我们创建的原始值、对象、函数……这一切都会占用内存。 当我们不再需要某个东西时会发生什么?JavaScript 引擎如何发现它并清理它? 可达性(Reachability) JavaScript 中主要的内存管理概念是 可达性。 简而言之,“可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。 这里列出固有的可达值的

  • 垃圾收集,引用计数,显式分配 和所有的现代语言一样,OCaml提供垃圾收集器,所以你不用像C/C++一样显式地分配和释放内存。 JWZ在他的文章 "Java sucks" rant(Java蛋疼(怒)!): 第一个好家伙是Java没有 free()。其他的都没有所谓了。这几乎掩盖了所有的缺点,不管有多糟糕, 这个有点让后续文档基本都没有意义了,但是...(译注:但是啥大家自己看吧) OCaml的垃

  • 问题内容: 这是我到目前为止所读的内容,如果我写错了,请更正我: Node.js基于V8 JavaScript引擎。 V8 JavaScript引擎实现了世界垃圾收集 这导致Node.js有时完全关闭几秒钟到几分钟来处理垃圾回收。 如果这是为生产代码运行的,那么对于10,000个用户而言,这是几秒钟。 这在生产环境中真的可以接受吗? 问题答案: 是否可接受取决于您的应用程序和堆大小。Big Gc约

  • Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器(GC),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 runtime 包访问 GC 进程。 通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立