在咱們的多個線上遊戲項目中,不少模塊和服務爲了提升響應速度,都在內存中存放了大量的(緩存)數據以便得到最快的訪問速度。git
一般狀況下,爲了使用方便,使用了 go 自身的 map 做爲存放容器。當有超過幾十萬 key 值,而且 map 的 value 是一個複雜的 struct
時,額外引入的 GC 開銷是沒法忽視的。在 cpu 使用統計圖中,咱們老是觀測到較爲規律的短期峯值。這個峯值在使用 1.3 版本的 go 中顯得特別突出(stop the world
問題)。後續版本 go gc 不斷優化,到咱們如今使用的 1.10 已是很是快速的併發 gc 而且只會有很短暫的 stw
。github
不過在各類 profile 的圖中,咱們依然觀察到了大量的 runtime.scanobject
開銷!golang
在一個14年開始的討論中,就以及發現了 大 map 帶來(特別是指針做爲 value 的 map)的 gc 開銷。遺憾的是在 2019 年的今天這個問題仍然存在。緩存
在上述的討論帖子中,有一個 Contributor randall77 提到:併發
Hash tables will still have overflow pointers so they will still need to be scanned and there are no plans to fix that.
不明白他的 overflow pointers
指的什麼,可是看起來若是你有一個大的,指針做爲 value 的 map 時,gc 的 scanobject
耗時就不會少。性能
因此咱們項目裏面本身弄了一個名爲 slice_map
的東西來專門優化內存中巨大的 map。這個 map 的實現機制是基於一下幾個觀察到的現象:測試
map[int]*obj
gc 極慢map[int]int
gc很是快[]*obj
gc 也很快因而咱們使用一個 []interface
來存放數據,map[int]int
作一個 key -> slice
來映射 key 到存放數據的 slice 的下標的索引。
最初的版本,刪除 key 以後,留下的 slice 的空間資源,使用了一個 freelist 來維護管理,但這個方案的問題在於:一旦系統中爆發大量突發性的插入將 slice 撐大
,後面就再也沒有機會回收內存了。優化
因此後面的版本使用了 挪動代替刪除 的操做,將騰出的空間移動到末尾(一個 O(1) 的操做),再在合適的時機回收 slice 後面沒有使用的空間(Shrink
操做),能夠防止內存的浪費。指針
這樣,既獲得了 便宜 的 gc,又得到了 map 的便利性。code
這個項目放到了 github 上: legougames/slice_map
在自帶的性能測試中,額外收穫了幾點:
FastIter
,遍歷的速度快1個數量級(並且仍是穩定的)。Iter
,那麼能夠在遍歷的過程當中刪除 key。