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


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

相关推荐
编程零零七2 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
2401_858286113 小时前
52.【C语言】 字符函数和字符串函数(strcat函数)
c语言·开发语言
铁松溜达py3 小时前
编译器/工具链环境:GCC vs LLVM/Clang,MSVCRT vs UCRT
开发语言·网络
everyStudy3 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
C-SDN花园GGbond4 小时前
【探索数据结构与算法】插入排序:原理、实现与分析(图文详解)
c语言·开发语言·数据结构·排序算法
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
迷迭所归处5 小时前
C++ —— 关于vector
开发语言·c++·算法
架构文摘JGWZ5 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
leon6255 小时前
优化算法(一)—遗传算法(Genetic Algorithm)附MATLAB程序
开发语言·算法·matlab
拾光师6 小时前
spring获取当前request
java·后端·spring