使用 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) 持续时间始终很低,而非之前版本相对于字符串的大小而增长。


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

相关推荐
微信-since81192几秒前
[ruby on rails] 安装docker
后端·docker·ruby on rails
色空大师5 分钟前
23种设计模式
java·开发语言·设计模式
Bruce小鬼18 分钟前
QT文件基本操作
开发语言·qt
2202_7544215424 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热31 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
懷淰メ40 分钟前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
宁静@星空1 小时前
006-自定义枚举注解
java·开发语言
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
武子康1 小时前
Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据库·sql·mybatis·springboot