为了账号安全,请及时绑定邮箱和手机立即绑定

在 Go 项目中实习

标签:
Go 职场生活

Go 1.23 带来了一个新的包 [unique](https://pkg.go.dev/unique),实现了interning,并且有一篇关于它的博客文章。Interning 是通过在内存中重用等值的对象,而不是保留重复的等值对象,来减少内存使用。其目的是为了减少内存使用。

一个程序例子

该源代码托管在GitHub上,网址是这里。这个程序展示了unique的一个实际应用场景。

内存里的一段长字符串。

在这个例子中,我有一个非常大的纯文本文件,并将其完全加载到内存中。这使得名为book的字符串变量占用282 MiB的内存。

读取数据和错误状态:data, err := os.ReadFile(`./large_book.txt`)
如果错误不为 nil {
    记录致命错误:log.Fatal(err)
}
将数据转换为字符串并赋值给book:book := string(data)

内存使用情况将通过辅助函数 mem() 进行监控,该函数执行一次完整的垃圾回收,并显示剩余内存。

    func mem() {  
        runtime.GC()  
        runtime.ReadMemStats(&memstat)  
        const MiB = 1024 * 1024  
        fmt.Println("当前程序正在使用", memstat.Alloc/MiB, "MiB")  
    }

在加载文件之后,这是 mem() 函数的输出:

    程序现在用了282 MiB

假设我想构建一个包含所有以'B'开头的单词的单词切片 Bwords。在我的样本书中,大约有2%的单词是以“B”开头的。一旦我有了这些以B开头的单词,我就不需要那本书的全文了,我就想把那本书的内存释放掉。

让我们来看看三种不同的方法来达到这个目的。

大字符串的片段

源代码位置: [nointerning.go](https://github.com/Deleplace/microbenchmarks/blob/master/interning/nointerning.go#L56)

在单个循环中,我找到 book 中的所有单词,并把它们中以 “B” 开头的放进 Bwords 里。

从book中提取从索引a到i的单词word。
如果word的第一个字符是'b'或者是'B',
将word添加到Bwords列表中。

在 Go 中,字符串是不可改变的,可以安全地使用 book[a:i] 来获取较大字符串中的一个小片段。

如你所见,Bwords 包含了一些指向大字符串 book 内部片段的指针。只要我们继续使用 Bwordsbook 的内存就不会被垃圾回收机制(GC)回收。

程序现在用了299兆字节

另外,书籍 books 仍然占用 282 MiB,除此之外,Bwords 分配了 17 MiB 的字符串头给它。

克隆字符串

来源: [nointerning_Clone.go](https://github.com/Deleplace/microbenchmarks/blob/master/interning/nointerning_Clone.go#L57),点击链接查看。

这个功能 [strings.Clone](https://pkg.go.dev/strings#Clone) 就是为了“仅保留一个大字符串中的一个小片段”。

    单词 := 书籍[索引:索引]  
    if 单词[0] == 'b' || 单词[0] == 'B' {  
        复制 := strings.Clone(单词)  
        Bwords = append(Bwords, 复制)  
    }

在最后一次使用 books 之后调用 mem(),但在最后一次使用 Bwords 之前,会输出

    程序现在占用 23 MiB

23 MiB 包括所有小克隆字符串内容的总大小,加上 Bwords 数组所占的大小,加上一些运行时的额外开销。这表明较大的字符串 book 已经被垃圾回收机制清理了。

内部字符串

参考来源: 参见[interning.go](https://github.com/Deleplace/microbenchmarks/blob/master/interning/interning.go#L57)

你有没有注意到词“beaucoup”被多次复制,在内存中产生了相同的拷贝?

使用这个新的 [unique](https://pkg.go.dev/unique) 包时,相同的字词将会变成一个共享的内部对象。

    word := book[a:i]  
    如果 word[0] 等于 'b' 或者 'B',则  
        handle := unique.Make(word)  
        Bwords = append(Bwords, handle)  

在最后一次使用 books 之后调用 mem(),但在最后一次使用 Bwords 之前,会输出。

程序现在用了8兆字节

8 MiB 是 独特的 以 'B' 开头的词的大小,加上 Bwords 的大小(其中包含许多重复的处理程序),再加上一些运行时的额外开销。

原来的长字符串 book 已经被成功释放了,因为 unique.Make 的实现 会自动复制 你给它的任何字符串,所以不必担心原始字符串会被引用。因此,字符串池不会保持对原始字符串的引用。

,在 Go 1.23.2 发布了一个小的修正之后,这才是正确的。在 Go 1.23 的最初两个小版本更新中,intern 池意外地保留了原始字符串的引用。

只是字符串吗?

unique 接受任何可以使用 == 进行比较的对象作为输入:例如,数字、字符串、数组、指针,或者你自定义的仅由可比较字段组成的结构。

字符串是唯一支持_切片操作_的可比较类型,unique包必须小心处理这种切片,以避免意外保留非常长字符串的子串引用。其他可比较类型则完全没有这种切片问题。

在公告博客文章中提到的IP地址结构体类型是一个很好的自定义数据类型内联化示例。

服务器等等

处理类似字符串的常见场景是一个状态化的Web服务器(例如运行几周)。当请求包含许多相似性的结构化数据时,使用字符串共享可能有助于你降低内存使用量,运行更节省资源的实例,从而减少内存占用,并节省资金。

更一般地说,任何将内存视为宝贵资源的执行环境(批处理作业、嵌入式系统等)都可以从对象 intern 化中获益。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消