這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。Slice 和 Map 是 Go 中的兩種重要的資料類型。本文將記錄我關於這兩種資料結構效能的一些關鍵的發現。在討論效能方面之前,我們先來簡單介紹一下 Slice 和 Map。**Slice:**Slice 是構建在數組之上的一種抽象資料結構。Slice 擁有一個指向數組開始位置的指標、數組長度以及 Slice 可以使用該數組的最大容量。Slice 可以按需增長或收縮。Slice 的增長通常包括為底層的數組重新分配記憶體。像 copy 和 append 這樣的函數可以協助增長數組。**Map:**Go 中的 Map 和其他語言類似(內部實現可能會有所不同)。Go 中的 Map 建立 bucket (每個 bucket 可以容納 8 個鍵)。**效能統計:**我對這兩種資料結構都進行了一些基準測試,結果記錄如下。**TEST-1**:尋找 Slice 中的一個 **INT** 元素 vs 尋找 Map 中的一個元素 -這裡,我們試著在一個長度為 n 的 Slice 中尋找一個元素,並與 Map 中的鍵尋找相比較。要尋找 Slice 中的一個元素,我們將遍曆該 Slice,然後進行簡單的 `if` 來檢查元素。至於 Map,我們簡單尋找鍵。在所有測試中,我都會尋找最壞的情況。總樣本 | len_size ( n ) | f(n)- []int (o(N)) - for 迴圈和 if(尋找最後一個元素)| map[int]int 直接尋找 o(1) ---|---|---|--- 2 million | 5 | 5.42 ns/op | 12.7 ns/op 2 million | 10 | 8.19 ns/op | 17.8 ns/op 2 million | 100 | 63.3 ns/op | 16.5 ns/op 2 million | 200 | 118 ns/op | 16.6 ns/op 2 million | 400 | 228 ns/op | 18.4 ns/op 2 million | 1000 | 573 ns/op | 17.0 ns/op 2 million | 10000 | 5674 ns/op | 17.6 ns/op 2 million | 100000 | 55141 ns/op | 15.1 ns/op 正如你所見的(不出所料),對於不同的 n,Map 尋找是常數複雜度(O(1))。然而,有趣的是,對於 n 較小的 Slice,簡單的 _for 迴圈和 if_ 比較花費的時間比 Map 少。較大的 n 如期望那樣花費更多的時間。**結論:對於尋找一個給定的鍵,我建議使用 Map。而對於一個較小的大小(n),使用 Slice 仍然是可以的。****TEST-2**:尋找 Slice 中的一個 **STRING** 元素 vs 尋找 Map 中的一個字串鍵 -我們所進行的步驟與 TEST-1 完全相同,這裡唯一的不同是,我們使用字串(String)。總樣本 | len - size ( n ) | f(n) []string [for 迴圈和 if(尋找最後一個元素)] | f(n) map[string]string ---|---|---|--- 2 million | 5 | 30.4 ns/op | 32.7 ns/op 2 million | 10 | 56.5 ns/op | 23.5 ns/op 2 million | 100 | 128 ns/op | 25.7 ns/op 2 million | 200 | 665 ns/op | 23.6 ns/op 2 million | 400 | 1766 ns/op | 23.7 ns/op 2 million | 1000 | 905 ns/op | 25.7 ns/op 2 million | 10000 | 8488 ns/op | 24.4 ns/op 2 million | 100000 | 82444 ns/op | 25.9 ns/op 從上面的資料,我們看到,給定一個鍵,尋找一個字串(使用 Map),具有 O(1) 複雜度。對於字串比較,Map 擊敗了 Slice。**結論:給定一個字串類型的鍵進行尋找,我推薦使用 Map。即使對於較小的 n ,使用 Map 也是不錯的。****TEST-3:** 給定索引,尋找 Slice 元素。如果我們知道索引,那麼,在 Go 中尋找 Slice 類似於在任何語言中尋找數組,並且如它一樣簡單。總樣本 | len - size | **[]int**(直接索引尋找) - O(1) | **[]string**(直接索引尋找) - O(1) | map[int]int 直接尋找 o(1) | map[string]string o(1) ---|---|---|---|---|--- 2 million | 5 | 0.30 ns/op | 0.29 ns/op | 12.7 | 32.7 2 million | 10 | 0.29 ns/op | 0.29 ns/op | 17.8 | 23.5 2 million | 100 | 0.29 ns/op | 0.29 ns/op | 16.5 | 25.7 2 million | 200 | 0.29 ns/op | 0.29 ns/op | 16.6 | 23.6 2 million | 400 | 0.29 ns/op | 0.29 ns/op | 18.4 | 23.7 2 million | 1000 | 0.29 ns/op | 0.29 ns/op | 17 | 25.7 2 million | 10000 | 0.58 ns/op | 0.57 ns/op | 17.6 | 24.4 2 million | 100000 | 0.58 ns/op | 0.55 ns/op | 15.1 | 25.9 如上所示,Slice 的直接尋找是 O(1) 固定增長率。**結論:直接尋找都是常數複雜度,所以,假定你知道索引,那麼使用哪個都無所謂。但是,假定索引已知,那麼 Slice 或者數組尋找仍然比 Map 尋找快得多。****TEST-4**:遍曆 Slice vs 遍曆 Map這裡,我試著遍曆 Map 和 Slice,並且在迴圈內執行一個常量操作。總複雜度將保持為 O(N)。總樣本 | len - size ( n ) | 遍曆 Int slice O(N) | 遍曆 int Map [O(N)](https://confluence.walmart.com/pages/createpage.action?spaceKey=SWTF&title=O%28N%29) ---|---|---|--- 2 million | 5 | 9.02 ns/op | 107 ns/op 2 million | 10 | 12.5 ns/op | 196 ns/op 2 million | 100 | 59.2 ns/op | 1717 ns/op 2 million | 200 | 84.9 ns/op | 3356 ns/op 2 million | 400 | 155 ns/op | 6677 ns/op 2 million | 1000 | 315 ns/op | 18906 ns/op 2 million | 10000 | 2881 ns/op | 178804 ns/op*** 2 million | 100000 | 29012 ns/op | 1802439 ns/op*** 如上所示,遍曆 Slice 比遍曆 Map 快了近 20 倍。原因是,跟 Map 不一樣,Slice (通過數組抽象出來)是在一個連續的記憶體塊上建立的。至於 Map,迴圈必須遍曆鍵空間(在 Go 中稱為 bucket),並且記憶體配置可能並不連續。這就是為什麼每次啟動並執行時候,遍曆 Map 的結果以不同的順序顯示索引值。**結論:如果要求是在整個元素列表上列印或者執行操作,而不是尋找,那麼 Slice 是最佳選擇。出於上述原因,遍曆 Map 會花費更多時間。**此外,請注意,就像對 Map 進行插入一樣,Slice 上的 append 操作保證了 O(1) 複雜度,但是這是一種 **攤銷(amortized)** 常量。append 可能偶爾需要為底層數組重新分配記憶體。( *** ) - 因為對於大 Map,Go 的基準函數逾時,所以樣本大小從 200 萬縮減至 2000。有關測試的細節:系統詳情 | go作業系統:darwin | Go-1.9.2 ---|---|--- MAC-OSX | go架構:amd64 | 原始碼:```go// m-c02jn0m1f1g4:slicevsmap user1$ cat slicemap.go package slicemap// 你可以取消列印行前的注釋來看看結果(確認代碼是否正常)。// 但是對於基準測試,我們不關心列印的結果。我們關心的是它啟動並執行時間// import "fmt"func RangeSliceInt(input []int, find int) (index int) { for index,value := range input { if (value == find) { //fmt.Println("found at",index) return index } } return -1}func RangeSliceIntPrint(input []int) { for _,_ = range input { continue }}func MapLookupInt(input map[int]int, find int) (key,value int) { if value,ok := input[find];ok { //fmt.Println("found at", find,value) return find,value } return 0,0}func MapRangeInt(input map[int]int) { for _,_ = range input { continue }}func DirectSliceInt(input []int, index int) int { return input[index]}// 對於字串 //func RangeSliceString(input []string, find string) (index int) { for index,value := range input { if (value == find) { // fmt.Println("found at",index) return index } } return -1}func RangeSliceStringPrint(input []string) { for _,_ = range input { continue }}func MapLookupString(input map[string]string, find string) (key,value string) { if value,ok := input[find];ok {// fmt.Println("found at", find,value) return find,value } return "0", "0"}func MapRangeString(input map[string]string) { for _,_ = range input { continue }}func DirectSliceString(input []string, index int) string { return input[index]}``````go// m-c02jn0m1f1g4:slicevsmap user1$ cat slicemap_test.gopackage slicemapimport ( "testing" "strconv")func Benchmark_TimeRangeSliceInt(b *testing.B) { b.StopTimer() input := make([]int, 100000, 500000) for i := 0; i < 100000; i++ { input[i] = i+10 } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for i := 0; i < b.N; i++ { RangeSliceInt(input, 100009) // 對於最壞情況,檢查最後一個元素 }}func Benchmark_TimeDirectSliceInt(b *testing.B) { b.StopTimer() input := make([]int, 100000, 500000) for i := 0; i < 100000; i++ { input[i] = i+10 } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次 fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for i := 0; i < b.N; i++ { DirectSliceInt(input, 99999) // 直接檢查索引值。o(1)。發送索引 }}func Benchmark_TimeMapLookupInt(b *testing.B) { b.StopTimer() input := make(map[int]int) // 扔一些值到 Map 中 for i := 1; i <=100000; i++ { input[i] = i+10 } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for k := 0; k < b.N; k++ { MapLookupInt(input, 100000) } /* 運行命令: go test -bench=Benchmark_TimeMapLookup */}func Benchmark_TimeSliceRangeInt(b *testing.B) { b.StopTimer() input := make([]int, 5, 10) for i := 0; i < 5; i++ { input[i] = i+10 } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for k := 0; k < b.N; k++ { RangeSliceIntPrint(input) }}func Benchmark_TimeMapRangeInt(b *testing.B) { b.StopTimer() input := make(map[int]int) // 扔一些值到 Map 中 for i := 1; i <=100000; i++ { input[i] = i+10 } b.StartTimer() b.N = 2000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for k := 0; k < b.N; k++ { MapRangeInt(input) }}// 測試字串func Benchmark_TimeRangeSliceString(b *testing.B) { b.StopTimer() input := make([]string, 100000, 500000) for i := 0; i < 100000; i++ { input[i] = strconv.FormatInt(int64(i+10),10) } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for i := 0; i < b.N; i++ { RangeSliceString(input, "100009") // 對於最壞情況,檢查最後一個元素 }}func Benchmark_TimeDirectSliceString(b *testing.B) { b.StopTimer() input := make([]string, 100000, 500000) for i := 0; i < 100000; i++ { input[i] = strconv.FormatInt(int64(i+10),10) } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for i := 0; i < b.N; i++ { DirectSliceString(input, 99999) // 直接檢查索引值。o(1) }}func Benchmark_TimeMapLookupString(b *testing.B) { b.StopTimer() input := make(map[string]string) // 扔一些值到 Map 中 for i := 1; i <=100000; i++ { input[strconv.FormatInt(int64(i),10)] = strconv.FormatInt(int64(i+10),10) } b.StartTimer() b.N = 2000000 // 只是為了避免數百萬次fmt.Println(以防你在 slicemap.go 包中進行 fmt.Println) for k := 0; k < b.N; k++ { MapLookupString(input, "100000") } /* 運行: go test -bench=Benchmark_TimeMapLookupString */}```
via: https://boltandnuts.wordpress.com/2017/11/20/go-slice-vs-maps/
作者:qwerty.ytrewq86 譯者:ictar 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1017 次點擊 ∙ 1 贊