编程 Go 1.23 中的新包:unique

2024-11-18 12:32:57 +0800 CST views 510

Go 1.23 中的新包:unique

unique 包提供了一些工具,用来对“可比较的值”进行规范化处理(即“驻留”)。

具体来说,“规范化”(canonicalization)或“驻留”(interning)指的是将多个相同的值(例如相同内容的字符串或结构体)通过某种机制合并成一个唯一的副本。这样,当有多个相同的值时,它们在内存中只会保存一个规范化的版本,其他相同的值都指向这个唯一的副本,从而节省内存并加速相等性比较操作。

在 Go 官方博客上,unique 包的主刀 Michael Knyszek 写了一篇关于 unique 包的介绍,并介绍了实现这个包过程中一些新发现(弱指针、finalizer 替代者)。

一个简单的驻留实现

从宏观上看,驻留非常简单。以下代码示例展示了如何通过普通的 Map 来对字符串进行去重。

var internPool map[string]string

// Intern 返回一个与 s 相等的字符串,但可能与之前传给 Intern 的字符串共享存储。
func Intern(s string) string {
    pooled, ok := internPool[s]
    if !ok {
        // 克隆字符串以防它是某个更大字符串的一部分。
        pooled = strings.Clone(s)
        internPool[pooled] = pooled
    }
    return pooled
}

当你构建许多可能重复的字符串时,例如解析文本格式时,这非常有用。然而,这种实现虽然简单,但存在一些问题:

  • 它永远不会从对象池中移除字符串。
  • 它无法安全地在多个 goroutine 中并发使用。
  • 它仅适用于字符串,而这个想法其实是普遍适用的。

此外,这个实现还错过了一个微妙的优化机会。字符串在底层是不可变的结构,包含一个指针和一个长度。当比较两个字符串时,如果指针不相等,就必须比较它们的内容以确定是否相等。但如果我们知道两个字符串是规范化的,那么只需比较它们的指针即可。

引入 unique 包

新引入的 unique 包提供了一个类似于 Intern 的函数 Make,它的工作方式与 Intern 类似。在内部,它也有一个全局 Map(一个快速的泛型并发 Map),并在该 Map 中查找值。然而,它与 Intern 有两个重要的区别:

  1. 首先,它接受任何可比较类型的值。
  2. 其次,它返回一个包装值 Handle[T],可以从中检索规范化的值。

Handle[T] 是设计的关键。Handle[T] 只有当用来创建它的两个值相等时,两个 Handle[T] 才相等。更重要的是,两个 Handle[T] 的比较是非常廉价的:只需进行指针比较。相比之下,比较两个长字符串的成本要高得多!

到目前为止,这些功能都可以通过普通的 Go 代码实现。然而,Handle[T] 还有第二个作用:只要某个值存在一个 Handle[T],Map 就会保留该值的规范化副本。一旦所有 Map 到特定值的 Handle[T] 都消失,该包就会将内部 Map 项标记为可删除,供垃圾回收器在未来回收。

如果你曾经使用过 Lisp,这一切可能听起来很熟悉。Lisp 中的符号是驻留的字符串,但它们本身并不是字符串,所有符号的字符串值都保证位于同一个池中。这种符号与字符串的关系类似于 Handle[string]string 的关系。

一个实际例子

如何使用 unique?可以看看标准库中的 net/netip 包,它对 netip.Addr 结构中的 addrDetail 类型的值进行了驻留。以下是 net/netip 中实际代码的简化版本,它使用了 unique 包。

type Addr struct {
    // 与地址相关的详细信息,被打包在一起并进行了规范化。
    z unique.Handle[addrDetail]
}

type addrDetail struct {
    isV6   bool   // 如果是 IPv4,则为 false;如果是 IPv6,则为 true。
    zoneV6 string // 如果是 IPv6,可能不等于 ""。
}

var z6noz = unique.Make(addrDetail{isV6: true})

// WithZone 返回一个与 ip 相同的 IP,但带有指定的 zone。如果 zone 为空,则移除 zone。
func (ip Addr) WithZone(zone string) Addr {
    if !ip.Is6() {
        return ip
    }
    if zone == "" {
        ip.z = z6noz
        return ip
    }
    ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone})
    return ip
}

由于许多 IP 地址可能使用相同的 zone,且该 zone 是它们标识的一部分,因此对它们进行规范化非常合理。Zone 的去重减少了每个 netip.Addr 的平均内存占用量,而它们被规范化后,比较 zone 名称只需进行简单的指针比较,这使得值的比较更加高效。

关于字符串驻留的注脚

尽管 unique 包很有用,但它与字符串的驻留不太一样,因为为了防止字符串被从内部 Map 中删除使用 Handle[T] 是必须的。这意味着你需要修改代码以同时保留 Handle[T] 和字符串。

但字符串特殊之处在于,虽然它们表现得像值,但实际上它们的底层包含指针。因此,理论上可以只对字符串的底层存储进行规范化,而将 Handle[T] 的细节隐藏在字符串内部。因此,未来仍然有可能实现所谓的透明字符串驻留,即可以在不需要 Handle[T] 的情况下对字符串进行驻留,类似于 Intern 函数,但语义更像 Make

目前,unique.Make("my string").Value() 是一种可能的解决方法。即使没有保留 Handle[T],字符串也会被允许从 unique 的内部 Map 中删除,但不会立即删除。实际上,条目至少会在下一次垃圾回收完成后才被删除,因此这种解决方法在回收之间的时间段内仍然允许一定程度的去重。

一些历史与展望

事实上,net/netip 包自引入以来就已经对 zone 字符串进行了驻留。它使用的驻留包是 go4.org/intern 的内部副本。与 unique 包类似,它有一个 Value 类型(在泛型之前看起来很像 Handle[T]),其内部 Map 中的条目会在不再被引用后被移除。

为了实现这种行为,旧的 intern 包做了一些不安全的事情,特别是在运行时之外实现了弱指针。而弱指针是 unique 包的核心抽象。弱指针是一种不会阻止垃圾回收器回收变量的指针;当变量被回收时,弱指针会自动变成 nil。

在实现 unique 包时,我们为垃圾回收器添加了适当的弱指针支持。经过设计决策的考验后,我们惊讶地发现这一切竟然如此简单且直接。弱指针现在已经成为一个公开提案。

这项工作还促使我们重新审视终结器,最终提出了一个更易于使用且效率更高的终结器替代方案。随着可比较值的哈希函数即将推出,Go 中构建内存高效缓存的未来充满希望!

参考资料

  1. unique 包的介绍
  2. 泛型并发 Map
  3. go4.org/intern
  4. 公开提案
  5. 终结器替代方案
  6. 高效缓存
复制全文 生成海报 编程 Go语言 内存管理

推荐文章

微信内弹出提示外部浏览器打开
2024-11-18 19:26:44 +0800 CST
2025,重新认识 HTML!
2025-02-07 14:40:00 +0800 CST
deepcopy一个Go语言的深拷贝工具库
2024-11-18 18:17:40 +0800 CST
百度开源压测工具 dperf
2024-11-18 16:50:58 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
html一份退出酒场的告知书
2024-11-18 18:14:45 +0800 CST
java MySQL如何获取唯一订单编号?
2024-11-18 18:51:44 +0800 CST
如何使用go-redis库与Redis数据库
2024-11-17 04:52:02 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
Vue3中如何进行性能优化?
2024-11-17 22:52:59 +0800 CST
Rust async/await 异步运行时
2024-11-18 19:04:17 +0800 CST
Vue 3 是如何实现更好的性能的?
2024-11-19 09:06:25 +0800 CST
JavaScript 上传文件的几种方式
2024-11-18 21:11:59 +0800 CST
MyLib5,一个Python中非常有用的库
2024-11-18 12:50:13 +0800 CST
JavaScript 的模板字符串
2024-11-18 22:44:09 +0800 CST
paint-board:趣味性艺术画板
2024-11-19 07:43:41 +0800 CST
Python 基于 SSE 实现流式模式
2025-02-16 17:21:01 +0800 CST
程序员茄子在线接单