草庐IT

string - 垃圾回收和Go中指针的正确用法

coder 2023-06-27 原文

我来自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)
}

我可以修改它以使用指向Image结构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值会传递/复制此小struct值,无论string的长度如何,该值仅为2个字。在64位体系结构上,即使string具有一千个字符,也只有16个字节。

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

It works and my first question is what happens to the result data structure after I build the mapping in this way? Will the Image URL string fields be left in memory somehow and the rest of the result will be garbage collected? Or will the result data structure stay in memory until the end of the program because something points to its members?



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

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

因此,尽管使用指向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()可能(并且通常确实)分配了比需要的更大的 slice (考虑 future 的增长)。在完成“构建”过程之后,我们可以遍历tagToUrlMap映射并将这些片段“修剪”到所需的最低限度。

这是可以做到的:
for tagName, urls := range tagToUrlMap {
    if cap(urls) > len(urls) {
        urls2 := make([]string, len(urls))
        copy(urls2, urls)
        tagToUrlMap[tagName] = urls2
    }
}

关于string - 垃圾回收和Go中指针的正确用法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51959783/

有关string - 垃圾回收和Go中指针的正确用法的更多相关文章

  1. ruby-on-rails - 如何使用 instance_variable_set 正确设置实例变量? - 2

    我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击

  2. ruby-on-rails - 正确的 Rails 2.1 做事方式 - 2

    question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参

  3. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  4. ruby-on-rails - 使用 config.threadsafe 时从 lib/加载模块/类的正确方法是什么!选项? - 2

    我一直致力于让我们的Rails2.3.8应用程序在JRuby下正确运行。一切正常,直到我启用config.threadsafe!以实现JRuby提供的并发性。这导致lib/中的模块和类不再自动加载。使用config.threadsafe!启用:$rubyscript/runner-eproduction'pSim::Sim200Provisioner'/Users/amchale/.rvm/gems/jruby-1.5.1@web-services/gems/activesupport-2.3.8/lib/active_support/dependencies.rb:105:in`co

  5. ruby - 字符串文字中的转义状态作为 `String#tr` 的参数 - 2

    对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一

  6. ruby - 调用其他方法的 TDD 方法的正确方法 - 2

    我需要一些关于TDD概念的帮助。假设我有以下代码defexecute(command)casecommandwhen"c"create_new_characterwhen"i"display_inventoryendenddefcreate_new_character#dostufftocreatenewcharacterenddefdisplay_inventory#dostufftodisplayinventoryend现在我不确定要为什么编写单元测试。如果我为execute方法编写单元测试,那不是几乎涵盖了我对create_new_character和display_invent

  7. ruby - 从 String#split 返回的零长度字符串 - 2

    在Ruby1.9.3(可能还有更早的版本,不确定)中,我试图弄清楚为什么Ruby的String#split方法会给我某些结果。我得到的结果似乎与我的预期相反。这是一个例子:"abcabc".split("b")#=>["a","ca","c"]"abcabc".split("a")#=>["","bc","bc"]"abcabc".split("c")#=>["ab","ab"]在这里,第一个示例返回的正是我所期望的。但在第二个示例中,我很困惑为什么#split返回零长度字符串作为返回数组的第一个值。这是什么原因呢?这是我所期望的:"abcabc".split("a")#=>["bc"

  8. ruby - 如何在 RVM 下将 Bundler 安装到 @global gemset,这是正确的方法吗 - 2

    我在OSX上(如果重要的话)。如果我使用RVM安装Ruby,它会默认将Bundler安装到@globalgemset假设我想要一个不同版本的bundler。我假设我需要做的就是执行geminstallbundler--version但是,这会将bundler安装到默认gemset并且RVM不会为其设置路径。因此,如果我键入bundler,它仍会启动一个与Ruby一起安装到@global中的bundler两个问题:如何将bundler安装到@globalgemset。将bundler安装到@globalgemset中的模式是否正确,或者我遗漏了什么 最佳答案

  9. ruby-on-rails - 如何正确格式化字符串,如 'mccdougal' 到 'McDougal' - 2

    什么Ruby或RailsDSL会将字符串"mccdougal"格式化为"McDougal",同时留下字符串"McDougal"原样?将titleize传递给"McDougal"结果如下:"McDougal".titleize#=>"McDougal" 最佳答案 据我所知,没有可以处理这种情况的Rails助手。这是一个非标准的边缘案例,需要特殊处理。但是,您可以创建自定义字符串变形。您可以将这段代码放入初始化程序中:ActiveSupport::Inflector.inflections(:en)do|inflect|inflect.

  10. ruby-on-rails - 我如何告诉 Rails 如何正确地大写首字母缩写词? - 2

    我的一个模型中有一个名为sui的字段。它代表“标准用户标识符”。当该字段出现验证错误时,Rails会打印“Suiisrequired”或“Suiisalreadytaken”。如何告诉Rails'sui'.titleize是“SUI”?我查看了Inflector.human,但这并不完全正确。 最佳答案 在这种情况下,我使用custom_err_msg插入。安装后,您可以提供如下自定义错误消息:validates_presence_of:sui,:message=>'^SUIisrequired'当您将^放在开头时,Rails不会输

随机推荐