使用 Go 1.23 中的 unique pkg 减少分配并比较性能

我一直在研究即将发布的 Go 1.23 版本,因此我很想了解这个新 unique 软件包以及它旨在解决的问题。下面是对该调查的总结,我希望它对其他有兴趣了解更多信息的人有用。

1

Interning 和 Go

Interning,最初由 Lisp 引入的一个术语,是指在内存中只存储一个值并共享对它的唯一引用的过程,而不是分配多个副本并浪费内存。例如,Go 编译器已经在编译时执行了字符串常量的驻留,因此它不会分配多个相同的字符串,而是分配该字符串的单个实例并共享对它的引用,如下面的代码片段所示。

javascript 复制代码
package main


import (
	"fmt"
	"reflect"
	"unsafe"
)


const greet1 = "hello world"
const greet2 = "hello world"


func main() {
	a := greet1
	p := (*reflect.StringHeader)(unsafe.Pointer(&a))
	fmt.Println("a address:", *&p.Data)


	b := greet2
	p2 := (*reflect.StringHeader)(unsafe.Pointer(&b))
	fmt.Println("b address:", *&p2.Data)
}
javascript 复制代码
$ go run main.go


a address: 4310983661
b address: 4310983661

在 Go 1.23 之前,Interning 运行时值仅可通过第三方包获得。但是,从 Go 1.23 开始,Interning 现已通过新 unique 包包含在标准库中。

2

uniqueGo 1.23 中的包

现在可以使用 unique 包通过其 Handle 类型执行比较,该类型充当任何提供的(可比较)值的全局唯一标识,这意味着如果用于创建句柄的两个值的比较也相等,则两个句柄的比较结果完全相等。

javascript 复制代码
type Handle[T comparable] struct {}
func (h Handle[T]) Value() T {}
func Make[T comparable](value T) Handle[T] {}

在内部,该 Handle 类型由并发安全的 Map 支持,它充当读取缓存,在缓存中未检测到时存储唯一值并返回一个 Handle[T] 旨在作为值/字段而不是底层值传递的,为您节省额外的分配,并且导致更便宜的比较,因为您只会进行引用比较而不是值比较。

但是你不能用 Map 来实现相同的行为吗?

当然,使用自定义 Map 可以实现相同的驻留行为,以减少重复分配,但无法有效地处理垃圾收集。该 unique 包有一个"弱引用"的概念,这意味着垃圾收集器可以通过访问运行时内部结构在单个周期内进行清理,这是自定义滚动解决方案无法执行的。

3

如何使用该 unique 包

让我们看一个简单的例子,该例子受到 net/netip 包的启发,实际上使用该 unique 包来有效处理 IPv6 区域名称。

javascript 复制代码
package main


import (
	"unique"
)


type addrDetail struct {
	isV6   bool
	zoneV6 string
}


func main() {
	h1 := unique.Make(addrDetail{isV6: true, zoneV6: "2001:0db8:0001:0000:0000:0ab9:C0A8:0102"})


	// this addrDetail won't be allocated as it already exists in the underlying map
	h2 := unique.Make(addrDetail{isV6: true, zoneV6: "2001:0db8:0001:0000:0000:0ab9:C0A8:0102"})


	if h1 == h2 {
		println("addresses are equal")
	}
	
	// Value() returns a copy of the underlying value (ie, different memory address)
	println(h1.Value().zoneV6) 
}

4

处理比较性能

前面我提到,除了通过重复值删除减少不必要的分配之外,该 unique 包还可以降低对象比较的成本,这在比较大字符串、具有字符串字段的结构或数组时尤其明显,其中比较可以简化为简单的指针比较。让我们来看看。

下面我们有一个基准,它重复以逗号分隔的 IPv6 区域字符串,然后对两个相同的副本执行字符串比较,一个基准没有将字符串包装在类型中 Handle,另一个基准有。

javascript 复制代码
package main


import (
	"strings"
	"testing"
	"unique"
)


func BenchmarkStringCompareSmall(b *testing.B)  { benchStringComparison(b, 10) }
func BenchmarkStringCompareMedium(b *testing.B) { benchStringComparison(b, 100) }
func BenchmarkStringCompareLarge(b *testing.B)  { benchStringComparison(b, 1000000) }


func BenchmarkCanonicalisingSmall(b *testing.B)  { benchCanonicalising(b, 10) }
func BenchmarkCanonicalisingMedium(b *testing.B) { benchCanonicalising(b, 100) }
func BenchmarkCanonicalisingLarge(b *testing.B)  { benchCanonicalising(b, 1000000) }


func benchStringComparison(b *testing.B, count int) {
	s1 := strings.Repeat("2001:0db8:0001:0000:0000:0ab9:C0A8:0102,", count)
	s2 := strings.Repeat("2001:0db8:0001:0000:0000:0ab9:C0A8:0102,", count)
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		if s1 != s2 {
			b.Fatal()
		}
	}
	b.ReportAllocs()
}


func benchCanonicalising(b *testing.B, count int) {
	s1 := unique.Make(strings.Repeat("2001:0db8:0001:0000:0000:0ab9:C0A8:0102,", count))
	s2 := unique.Make(strings.Repeat("2001:0db8:0001:0000:0000:0ab9:C0A8:0102,", count))
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		if s1 != s2 {
			b.Fatal()
		}
	}
	b.ReportAllocs()
}

5

基准测试结果

让我们运行基准测试并检查结果:

javascript 复制代码
$ go test -run='^$' -bench=.


goos: darwin
goarch: arm64
pkg: go-experiment
cpu: Apple M1
BenchmarkStringCompareSmall-8     	116581837	        9.392 ns/op	      0 B/op	      0 allocs/op
BenchmarkStringCompareMedium-8    	14944300	       80.15 ns/op	      0 B/op	      0 allocs/op
BenchmarkStringCompareLarge-8     	903	               1296028 ns/op	      0 B/op	      0 allocs/op


BenchmarkCanonicalisingSmall-8    	1000000000	        0.3132 ns/op	      0 B/op	      0 allocs/op
BenchmarkCanonicalisingMedium-8   	1000000000	        0.3140 ns/op	      0 B/op	      0 allocs/op
BenchmarkCanonicalisingLarge-8    	1000000000	        0.3128 ns/op	      0 B/op	      0 allocs/op
javascript 复制代码
PASS
ok  	go-experiment	5.596s

运行基准测试我们可以看到,无论字符串的大小如何,无论是对 10 个副本还是 1,000,000 个副本的字符串进行比较,每个操作的纳秒数 (ns/op) 持续时间始终很低,而非之前版本相对于字符串的大小而增长。


随手关注或者"在看",诚挚感谢!

相关推荐
Eiceblue1 小时前
Python 合并 Excel 单元格
开发语言·vscode·python·pycharm·excel
幸好我会魔法2 小时前
人格分裂(交互问答)-小白想懂Elasticsearch
大数据·spring boot·后端·elasticsearch·搜索引擎·全文检索
SomeB1oody2 小时前
【Rust自学】15.2. Deref trait Pt.1:什么是Deref、解引用运算符*与实现Deref trait
开发语言·后端·rust
何中应2 小时前
从管道符到Java编程
java·spring boot·后端
情深不寿3173 小时前
C++----STL(list)
开发语言·c++
组合缺一3 小时前
Solon Cloud Gateway 开发:Route 的过滤器与定制
java·后端·gateway·reactor·solon
SomeB1oody3 小时前
【Rust自学】15.4. Drop trait:告别手动清理,释放即安全
开发语言·后端·rust
liruiqiang053 小时前
DDD-全面理解领域驱动设计中的各种“域”
开发语言·架构
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS贸易行业crm系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源
前端熊猫4 小时前
JavaScript 的 Promise 对象和 Promise.all 方法的使用
开发语言·前端·javascript