Go1.24 Map 引入了新的問題,預計將在 Go1.25 修復!
大家好,我是煎魚。
在之前 Go1.24 新特性中,Map 有了非常大的變化。在 Map 中使用 Swiss Table 來替換 Hashmap 的原始實現。
前文我在 《Go1.24 新特性:map 換引擎,性能顯著提高!》中有所介紹,帶來了顯著的綜合性能提高。
但也帶來了新的問題點,今天分享給大家。有興趣的同學可以關注后續進展。
問題點
近日 @Michael Pratt 發現了 Map 里引入的新問題點:
圖片
在 Go1.24 所引入的 swissmaps 中,map[int64]struct{} 的每個槽(slot)需要 16 字節空間,而不是預期的 8 字節。
而 map[int64]struct{} 是日常中用來占位表現最為常用的一個用法之一。
原因
造成這個現象的原因在于 Map 內部定義存儲[1]方式的所造成的副作用:
type group struct {
ctrl uint64
slots [abi.SwissMapGroupSlots]struct {
key keyType
elem elemType
}
}
原因在于:
- elemType 是 struct{}:
Go 編譯器中的 struct 大小規則規定,如果 struct 以零大小(zero-size)類型結束,則該字段將獲得 1 字節的空間。
會設計這個機制的目的是:防止有人創建指向最后一個字段的指針,Go 編譯器不希望指針指向分配結束的位置。
- keyType 需要 8 字節對齊。
劃重點:最后一個字段 keyType 實際上使用了整整 8 字節,這是 KVKVKVKV(K/V 一起存儲) 由于對齊要求而浪費空間的最極端情況。
這個問題出現在了新的 Map 新引入的 swissmaps 中。
解決思路
實際上還是 @Michael Pratt,既發現問題,還提出如何解決問題。簡直是專業的大好人!其提出了新的 issues《runtime: map cold cache improvements[2]》:
圖片
里面涉及的內容物比較多。聚焦于本文問題點,其主要通過:更改布局將所有鍵放在一起來解決這個問題。
K/V 放一起(KVKVKV)和所有 K 放一起(KKKVVV...,舊 Map 的做法),各有優缺點。
下面對比提到的控制字(control word)是指與哈希表中每個槽(slot)對應的一小段元數據,常用于加速查找過程。
以下是具體不同的布局方式的對比:
1、鍵/值(K/V)一起存儲:
- 命中(Map hit):
控制字可能發生緩存未命中。
鍵比較可能發生緩存未命中。
在父調用中使用值時不會發生緩存未命中。
- 未命中(Map miss):
- 控制字可能發生緩存未命中。
- 在誤判匹配(約 1%)時,鍵比較可能發生緩存未命中。
- 無值可用。
2、所有鍵(K)集中存儲:
- 命中(Map hit):
控制字可能發生緩存未命中。
鍵比較不會發生緩存未命中。
在父調用中使用值時可能發生緩存未命中(如果使用)。
- 未命中(Map miss):
- 控制字可能發生緩存未命中。
- 在誤判匹配(約 1%)時,鍵比較不會發生緩存未命中。
- 無值可用。
社區反饋
這在國外的社區開發者中也有提到:
圖片
反饋在 Go1.24 中 map[uint64]struct{} 的性能比 Go1.23 低 10%。
而 Prometheus 的維護者 @Bryan Boreham,在 Prometheus 項目中,也發現使用 Go1.24 時訪問大 Map 會使用更多的 CPU。
圖片
其具體的使用的基準測試結果如下:
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 占用時間為 82 秒;使用 Go1.24.2 版本,CPU 占用時間為 108 秒。
總結
目前來看,該項問題點有較大概率在 Go1.25 得到解決或者緩解。畢竟這是 Go1.24 新引入進來的問題。
大家也能看出來,軟件設計沒有銀彈。選擇了 A,就會有 B 的短板。很多事情也是這樣的。
后續我們將會繼續保持關注,看看 Go 官方團隊是否再次選擇把所有鍵(K)擺放在一起來解決這個問題點,又或是引入新的解決方案。
參考資料
[1] 內部定義存儲: 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