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
内部片段的指针。只要我们继续使用 Bwords
,book
的内存就不会被垃圾回收机制(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 化中获益。
共同学习,写下你的评论
评论加载中...
作者其他优质文章