Golang 中map與GC“糾纏不清”的關系
- Map是什么?
- GC是什么?
- GC定義
- GC中的垃圾怎么定義
- Map需要被GC回收嗎?
- 什么場景下會需要Map避開GC?
- 究竟影響了多少指標?
- 不同指標下的結果量化統計與分析
- 結論1
- 結論2
- 基于GC逃逸的map快取設計
- 思考
Map是什么?
map的定義:是能夠在時間復雜度為O(1)的情況下,對目標資料進行添加、洗掉、查詢的一種哈希表,采用的格式為Key-Value存盤,每一個value都對應的有一個key
GC是什么?
GC定義
GC 是一種垃圾收集器,目標物件是用戶程式在運行程序中的堆,也就是heap,旨在清楚程式運行完之后產生的垃圾,釋放記憶體以便回圈利用
GC中的垃圾怎么定義
一般情況來說記憶體分為堆和堆疊,堆疊的話是由作業系統和編譯器來進行垃圾回收(一般堆疊出問題了都是比較嚴重的事故),堆的話則是用程式來進行動態分配的,這里的“垃圾“主要指的就是在堆疊中、被分配的、且在函式執行的周期中沒有再被參考的物件,
Map需要被GC回收嗎?
不一定,參考go 1.5的解釋
After go version 1.5, if you use a map without pointers in keys and values, the GC will omit its content.
指標型別的資料結構包括啥? string型別,結構體型別,或者任何基本型別+指標的定義(*int, *float),甚至可以是回傳型別為指標的function
原理猜想:可能的原因是非指標的型別,例如int32,uint32,不會被分配到堆上,從而避免了被GC掃到,
什么場景下會需要Map避開GC?
這個問題等價于GC在什么場景下會比較影響效率,通常情況下,GC是對堆疊進行操作,并且在進行GC的時候會停掉整個程式的運行(但是在Go 1.5之后有concurrent GC,這里不做討論),那么實際上影響整個程式運行效率的實際上就是GC運行的時長,而GC運行的時長主要與程式動態分配的堆疊記憶體有關,所以可以推測:在申請了大量的堆疊記憶體的場景,例如設計百萬數量,甚至千萬數量級別的記憶體快取中,我們需要考慮GC回收所占用的延時,
究竟影響了多少指標?
讓我們做個實驗,探究在不同資料量+使用不同KV存盤型別的map的變數條件下,GC的時間長短,
資料量:
三個水位:十,一百,一千萬
map的kv型別有
var demo1 = make(map[int]*int)// KV型別->對應有指標的map
var demo2 = make(map[int]int) // KV型別->對應無指標的map
type demoWithOneE = struct {
E int
}
type demoWithTwoE = struct {
E int
E1 int
}
type demoWithThreeE = struct {
E int
E1 int
E2 int
}
相關測驗邏輯:
從1到N(N=10,100,1000000)分別給map賦值,賦值完成之后進行runtime.GC(),并記錄GC的時間
// TestForPointerMap 測驗指標map
func main() {
TestForPointerMapBenchMark()
TestForNonPointerMapBenchMark()
}
// TestForStructPointerMapBenchMark 測驗結構體指標map
func TestForStructPointerMapBenchMark(wg *sync.WaitGroup) {
defer wg.Done()
runtime.GC()
var demo1 = make(map[int]*demo) // KV型別->對應有指標的map
for i := 0; i < N; i++ {
v := i
... // 這里分別初始化三種不同的結構體,每種結構體的成員變數型別相同,個數不同
}
// 進行runtime.GC(),并統計時間
println("開始統計")
start := time.Now()
runtime.GC() // 開啟這一輪的GC
result := time.Since(start)
println(fmt.Sprintf("GC time: %v", result))
}
// TestForPointerMapBenchMark 測驗指標map
func TestForPointerMapBenchMark() {
runtime.GC()
var demo1 = make(map[int]*int) // KV型別->對應有指標的map
for i := 0; i < N; i++ {
v := i
demo1[i] = &v
}
// 進行runtime.GC(),并統計時間
println("開始統計")
start := time.Now()
runtime.GC() // 開啟這一輪的GC
result := time.Since(start)
println(fmt.Sprintf("GC time: %v", result))
}
// TestForNonPointerMapBenchMark 測驗指標map
func TestForNonPointerMapBenchMark() {
runtime.GC()
var demo2 = make(map[int]int) // KV型別->對應無指標的map
for i := 0; i < N; i++ {
demo2[i] = i
}
println("開始統計")
// 進行runtime.GC(),并統計時間
start := time.Now()
runtime.GC() // 開啟這一輪的GC
result := time.Since(start)
println(fmt.Sprintf("Non GC time: %v", result))
}
不同指標下的結果量化統計與分析
| 結果 | 無指標(int) | 有指標(int) | 有指標(demoWithOneE) | 有指標(demoWithTwoE) | 有指標(demoWithThreeE) |
|---|---|---|---|---|---|
| 10 | 103.76μs | 122.167μs | 148.924μs | 111.608μs | 151.322μs |
| 1000 | 235.812μs | 209.053μs | 150.502μs | 136.332μs | 109.873μs |
| 10000000 | 1.283464ms | 11.629275ms | 11.370434ms | 20.11954ms | 31.112292ms |
結論1
隨著KV數量級的增加,帶指標的map需要更多申請更多的堆空間,因此程式需要申請GC需要更多的時間來進行垃圾回收
結論2
在有指標的前提下,可以看到含有不同成員變數的結構體,所占用的GC時間也不相同,但從資料可以看出:
在大資料量的情況下,GC回收時間與結構體中的成員變數數量呈線性關系,原因是在結構體中,成員數量越多,單個結構體需要申請的記憶體就越大,
不過目前沒有嘗試過結構體中多種不同的成員變數型別的組合,但猜想由于記憶體對齊,實際實驗結果也會有類似的結論
基于GC逃逸的map快取設計
通常情況下map中使用string等指標型別的資料結構有助于簡化開發成本,同時也能解決大部分問題,但是在數百萬甚至千萬級別的記憶體快取中,GC的周期回收時間可以達到秒級,
因此,我們需要盡量避免map中存在指標,解決的辦法可以采用“二級索引”的思想:
我們動態申請記憶體(此處基于不同的逐出策略,需要做一定的記憶體申請策略的調整),之后建立map[int]int key為int表示的是邏輯key(可以定義為string型別,也可以定義為函式型別等等帶指標的資料結構)的哈希函式,其中value存放的為真正資料在記憶體中的offset,
例如,我們插入一個<"hello","world">的KV值,首先先將hello哈希成一串整形數字,再將world轉化為byte存盤在我們申請的記憶體中,由于world占用5個byte,所以offset=5

思考
- 對于沒有指標的map對應的case,其GC的回收時間也隨著KV數量的增大而增大,此處不符合預期,目前還在研究中,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/255589.html
標籤:區塊鏈
