Go1.24 Map 引入了新的問(wèn)題,預(yù)計(jì)將在 Go1.25 修復(fù)!
大家好,我是煎魚(yú)。
在之前 Go1.24 新特性中,Map 有了非常大的變化。在 Map 中使用 Swiss Table 來(lái)替換 Hashmap 的原始實(shí)現(xiàn)。
前文我在 《Go1.24 新特性:map 換引擎,性能顯著提高!》中有所介紹,帶來(lái)了顯著的綜合性能提高。
但也帶來(lái)了新的問(wèn)題點(diǎn),今天分享給大家。有興趣的同學(xué)可以關(guān)注后續(xù)進(jìn)展。
問(wèn)題點(diǎn)
近日 @Michael Pratt 發(fā)現(xiàn)了 Map 里引入的新問(wèn)題點(diǎn):
圖片
在 Go1.24 所引入的 swissmaps 中,map[int64]struct{} 的每個(gè)槽(slot)需要 16 字節(jié)空間,而不是預(yù)期的 8 字節(jié)。
而 map[int64]struct{} 是日常中用來(lái)占位表現(xiàn)最為常用的一個(gè)用法之一。
原因
造成這個(gè)現(xiàn)象的原因在于 Map 內(nèi)部定義存儲(chǔ)[1]方式的所造成的副作用:
type group struct {
ctrl uint64
slots [abi.SwissMapGroupSlots]struct {
key keyType
elem elemType
}
}
原因在于:
- elemType 是 struct{}:
Go 編譯器中的 struct 大小規(guī)則規(guī)定,如果 struct 以零大小(zero-size)類(lèi)型結(jié)束,則該字段將獲得 1 字節(jié)的空間。
會(huì)設(shè)計(jì)這個(gè)機(jī)制的目的是:防止有人創(chuàng)建指向最后一個(gè)字段的指針,Go 編譯器不希望指針指向分配結(jié)束的位置。
- keyType 需要 8 字節(jié)對(duì)齊。
劃重點(diǎn):最后一個(gè)字段 keyType 實(shí)際上使用了整整 8 字節(jié),這是 KVKVKVKV(K/V 一起存儲(chǔ)) 由于對(duì)齊要求而浪費(fèi)空間的最極端情況。
這個(gè)問(wèn)題出現(xiàn)在了新的 Map 新引入的 swissmaps 中。
解決思路
實(shí)際上還是 @Michael Pratt,既發(fā)現(xiàn)問(wèn)題,還提出如何解決問(wèn)題。簡(jiǎn)直是專(zhuān)業(yè)的大好人!其提出了新的 issues《runtime: map cold cache improvements[2]》:
圖片
里面涉及的內(nèi)容物比較多。聚焦于本文問(wèn)題點(diǎn),其主要通過(guò):更改布局將所有鍵放在一起來(lái)解決這個(gè)問(wèn)題。
K/V 放一起(KVKVKV)和所有 K 放一起(KKKVVV...,舊 Map 的做法),各有優(yōu)缺點(diǎn)。
下面對(duì)比提到的控制字(control word)是指與哈希表中每個(gè)槽(slot)對(duì)應(yīng)的一小段元數(shù)據(jù),常用于加速查找過(guò)程。
以下是具體不同的布局方式的對(duì)比:
1、鍵/值(K/V)一起存儲(chǔ):
- 命中(Map hit):
控制字可能發(fā)生緩存未命中。
鍵比較可能發(fā)生緩存未命中。
在父調(diào)用中使用值時(shí)不會(huì)發(fā)生緩存未命中。
- 未命中(Map miss):
- 控制字可能發(fā)生緩存未命中。
- 在誤判匹配(約 1%)時(shí),鍵比較可能發(fā)生緩存未命中。
- 無(wú)值可用。
2、所有鍵(K)集中存儲(chǔ):
- 命中(Map hit):
控制字可能發(fā)生緩存未命中。
鍵比較不會(huì)發(fā)生緩存未命中。
在父調(diào)用中使用值時(shí)可能發(fā)生緩存未命中(如果使用)。
- 未命中(Map miss):
- 控制字可能發(fā)生緩存未命中。
- 在誤判匹配(約 1%)時(shí),鍵比較不會(huì)發(fā)生緩存未命中。
- 無(wú)值可用。
社區(qū)反饋
這在國(guó)外的社區(qū)開(kāi)發(fā)者中也有提到:
圖片
反饋在 Go1.24 中 map[uint64]struct{} 的性能比 Go1.23 低 10%。
而 Prometheus 的維護(hù)者 @Bryan Boreham,在 Prometheus 項(xiàng)目中,也發(fā)現(xiàn)使用 Go1.24 時(shí)訪(fǎng)問(wèn)大 Map 會(huì)使用更多的 CPU。
圖片
其具體的使用的基準(zhǔn)測(cè)試結(jié)果如下:
go test -run XXX -bench 'BenchmarkLoadWLs' -cpuprofile=cpu.pprof ./tsdb/
goos: linux
goarch: amd64
pkg: github.com/prometheus/prometheus/tsdb
cpu: Intel(R) Core(TM) i7-14700K
BenchmarkLoadWLs/batches=1000,seriesPerBatch=10000,samplesPerSeries=50,exemplarsPerSeries=0,mmappedChunkT=0,oooSeriesPct=0.000,oooSamplesPct=0.000,oooCapMax=0,missingSeriesPct=0.000-28 1 25232921108 ns/op
PASS
ok github.com/prometheus/prometheus/tsdb 33.909s
使用 Go1.23 版本,runtime.mapaccess1_fast64 的 CPU 占用時(shí)間為 82 秒;使用 Go1.24.2 版本,CPU 占用時(shí)間為 108 秒。
總結(jié)
目前來(lái)看,該項(xiàng)問(wèn)題點(diǎn)有較大概率在 Go1.25 得到解決或者緩解。畢竟這是 Go1.24 新引入進(jìn)來(lái)的問(wèn)題。
大家也能看出來(lái),軟件設(shè)計(jì)沒(méi)有銀彈。選擇了 A,就會(huì)有 B 的短板。很多事情也是這樣的。
后續(xù)我們將會(huì)繼續(xù)保持關(guān)注,看看 Go 官方團(tuán)隊(duì)是否再次選擇把所有鍵(K)擺放在一起來(lái)解決這個(gè)問(wèn)題點(diǎn),又或是引入新的解決方案。
參考資料
[1] 內(nèi)部定義存儲(chǔ): https://cs.opensource.google/go/go/+/master:src/cmd/compile/internal/reflectdata/map_swiss.go;l=30
[2] runtime: map cold cache improvements: https://github.com/golang/go/issues/70835