Go: 通過代碼學習 Map 的設計 — Part II
Map Design
這篇文章是 "Go: 通過例子學習 Map 的設計 [1]" 的下一篇,它從高層次上介紹了 map 的設計。爲了理解下文討論的概念,我強烈建議你從上一篇文章開始閱讀。
map 的內部設計向我們展示了它如何優化性能和內存管理。讓我們從 map 的內存分配開始。
map 初始化
Go 提供了兩種初始化 map 和內部 bucket 的方式:
- 用戶明確定義了容量
m := make(map[string]int, 10)
- 第一次更新 map 的請求時初始化
m := make(map[string]int)
m[`foo`] = 1
在第二個例子中,map 的容量未指定,因此當 map m 創建時,不會創建 bucket,Go 會等待直到 map 的第一次更新時才初始化 map。因此,第二行會創建 bucket。
在上述兩個案例中,map 會根據我們的需要擴容。在第一個例子中,如果我們需要多於 10 個 key,預定義的容量不會阻止 map 的擴容,它只是幫助我們優化對 map 的使用,因爲按需擴容會有性能損耗。
按需擴容的影響
Go 足夠智能來對 map 進行按需擴容。然而,這種原生行爲是有代價的。讓我們運行一些基準測試,初始化兩個 map,並創建 100/1000 個鍵值對。前兩個基準測試使用初始化容量的 map,容量分別爲 100 和 1000,另一個使用容量未定義,即按需增長的 map:
// 100 allocations
name time/op
LazyInitMap100Keys-8 6.67 µ s ± 0%
InitMap100Keys-8 3.57 µ s ± 0%
name alloc/op
LazyInitMap100Keys-8 5.59kB ± 0%
InitMap100Keys-8 2.97kB ± 0%
name allocs/op
LazyInitMap100Keys-8 18.0 ± 0%
InitMap100Keys-8 7.00 ± 0%
// 1000 allocations
name time/op
LazyInitMap1000Keys-8 77.8 µ s ± 0%
InitMap1000Keys-8 32.2 µ s ± 0%
name alloc/op
LazyInitMap1000Keys-8 86.8kB ± 0%
InitMap1000Keys-8 41.2kB ± 0%
name allocs/op
LazyInitMap1000Keys-8 66.0 ± 0%
InitMap1000Keys-8 7.00 ± 0%
在這裏,我們很清晰的看到擴容和遷移 bucket 的代價,它多消耗了 80% 到 140% 的時間。內存消耗也受到了相同比例的影響。Go 對 map 進行了精妙的設計來減少內存消耗。
bucket 填充
正如之前看到的,每個 bucket 僅包含 8 個鍵值對。這裏有一件有趣的事情值得注意,Go 先存儲 key,後存儲 value:
img
這避免了填充齊導致的內存浪費。事實上,由於 key 和 value 的大小可能不同,最終可能導致大量的內存填充。下面是兩個 string / bool 對的例子,展示了 key 和 value 混在一起的情況:
made from Golang-sizeof.tips
如果 key 和 value 分別分組存放:
made from Golang-sizeof.tips
我們可以清楚的看到,填充被消除了很多。下面看一下如何訪問這些值。
數據訪問
Go 提供了兩種訪問 map 中數據的方式:
m := make(map[string]int)
v := m[`my_key`]
v, ok := m[`my_key`]
我們可以單獨訪問值,也可以攜帶一個布爾變量,用來表示是否在 map 中找到該值。我們可能會好奇,既然所有的返回值都應該明確的映射到一個變量,至少是 _
,怎麼可能會有兩種訪問方式。實際上,Go 生成的彙編代碼 [2] 會給我們提示:
(main.go:3) CALL runtime.mapaccess1_faststr(SB)
(main.go:4) CALL runtime.mapaccess2_faststr(SB)
我們可以看到,根據你訪問數據的方式,編譯器將會使用擁有正確簽名的兩個不同的內部方法:
func mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer
func mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool)
編譯器的這個小技巧非常有用,使我們可以靈活地訪問數據。其實編譯器甚至做的比這更好,它可以根據 map 的類型來選擇數據訪問方法。在這個例子中,我們的 map 使用 string 作爲 key,編譯器會選擇 mapaccess1_faststr
作爲數據訪問方法。後綴 str
表明了它對於 string 作爲 key 的 map 進行了優化。讓我們嘗試使用 integer:
m := make(map[int]int)
v := m[1]
v, ok := m[1]
彙編代碼會爲我們選擇如下方法:
(main.go:3) CALL runtime.mapaccess1_fast64(SB)
(main.go:4) CALL runtime.mapaccess2_fast64(SB)
這次,編譯器將會使用 int64(在 64 位機器上,是 int) 作爲 key 的專用方法。每個方法都會針對哈希比較進行優化,如果開發人員在分配 map 時未指定容量,惰性初始化也會被優化。
下一篇,也就是這個系列的最後一篇文章 "Go: 併發訪問 Map[3]",將會講解 map 在併發上下文中的表現。
via: https://medium.com/@blanchon.vincent/go-map-design-by-code-part-ii-50d111557c08
作者:blanchon.vincent[4] 譯者:DoubleLuck[5] 校對:polaris1119[6]
本文由 GCTT[7] 原創編譯,Go 中文網 [8] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
Go: 通過例子學習 Map 的設計: https://studygolang.com/articles/22773
[2]
彙編代碼: https://golang.org/doc/asm
[3]
Go: 併發訪問 Map: https://medium.com/@blanchon.vincent/go-concurrency-access-with-maps-part-iii-8c0a0e4eb27e
[4]
blanchon.vincent: https://medium.com/@blanchon.vincent
[5]
DoubleLuck: https://github.com/DoubleLuck
[6]
polaris1119: https://github.com/polaris1119
[7]
GCTT: https://github.com/studygolang/GCTT
[8]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wilXpQwFNeuF2f8b-eeARg