如果我們要遍歷某個陣列,Map集合,Slice切片等,Go語言(Golang)為我們提供了比較好用的For Range方式。range是一個關鍵字,表示範圍,和for配合使用可以迭代陣列,Map等集合。它的用法簡潔,而且map、channel等也都是用for range的方式,所以在編碼中我們使用for range
進行迴圈迭代是最多的。對於這種最常使用的迭代,尤其是和for i=0;i<N;i++
對比,效能怎麼樣?我們進行下示例分析,讓我們對for range
迴圈有個更深的理解,便於我們寫出效能更高的程式。
基本用法
for range
的使用非常簡單,這裡演示下兩種集合型別的使用。
package main
import "fmt"
func main() {
ages:=[]string{"10", "20", "30"}
for i,age:=range ages{
fmt.Println(i,age)
}
}
複製程式碼
這是針對 Slice 切片的迭代使用,使用range
關鍵字返回兩個變數i,age
,第一個是 Slice 切片的索引,第二個是 Slice 切片中的內容,所以我們列印出來:
0 10
1 20
2 30
複製程式碼
關於Go語言 Slice 切片的,可以參考我以前寫的這篇 Go語言實戰筆記(五)| Go 切片
下面再看看map(字典)的for range
使用示例。
package main
import "fmt"
func main() {
ages:=map[string]int{"張三":15,"李四":20,"王武":36}
for name,age:=range ages{
fmt.Println(name,age)
}
}
複製程式碼
在使用for range
迭代map的時候,返回的第一個變數是key
,第二個變數是value
,也就是我們例子中對應的name
和ages
。我們執行程式看看輸出結果。
張三 15
李四 20
王武 36
複製程式碼
這裡需要注意的是,for range map
返回的K-V
鍵值對順序是不固定的,是隨機的,這次可能是張三-15
第一個出現,下一次執行可能是王武-36
第一個被列印了。
關於Map更詳細的可以參考我以前的一篇文章 Go語言實戰筆記(六)| Go Map。
常規for迴圈對比
比如對於 Slice 切片,我們有兩種迭代方式:一種是常規的for i:=0;i<N;i++
的方式;一種是for range
的方式,下面我們看看兩種迭代的效能。
func ForSlice(s []string) {
len := len(s)
for i := 0; i < len; i++ {
_, _ = i, s[i]
}
}
func RangeForSlice(s []string) {
for i, v := range s {
_, _ = i, v
}
}
複製程式碼
為了測試,寫了這兩種迴圈迭代 Slice 切片的函式,從實現上看,他們的邏輯是一樣的,保證我們可以在同樣的情況下測試。
import "testing"
const N = 1000
func initSlice() []string{
s:=make([]string,N)
for i:=0;i<N;i++{
s[i]="www.flysnow.org"
}
return s;
}
func BenchmarkForSlice(b *testing.B) {
s:=initSlice()
b.ResetTimer()
for i:=0; i<b.N;i++ {
ForSlice(s)
}
}
func BenchmarkRangeForSlice(b *testing.B) {
s:=initSlice()
b.ResetTimer()
for i:=0; i<b.N;i++ {
RangeForSlice(s)
}
}
複製程式碼
這事Bench基準測試的用例,都是在相同的情況下,模擬長度為1000的 Slice 切片的遍歷。然後我們執行go test -bench=. -run=NONE
檢視效能測試結果。
BenchmarkForSlice-4 5000000 287 ns/op
BenchmarkRangeForSlice-4 3000000 509 ns/op
複製程式碼
從效能測試可以看到,常規的for迴圈,要比for range
的效能高出近一倍,到這裡相信大家已經知道了原因,沒錯,因為for range
每次是對迴圈元素的拷貝,所以集合內的預算越複雜,效能越差,而反觀常規的for迴圈,它獲取集合內元素是通過s[i]
,這種索引指標引用的方式,要比拷貝效能要高的多。
既然是元素拷貝的問題,我們迭代 Slice 切片的目的也是為了獲取元素,那麼我們換一種方式實現for range
。
func RangeForSlice(s []string) {
for i, _ := range s {
_, _ = i, s[i]
}
}
複製程式碼
現在,我們再次進行 Benchmark 效能測試,看看效果。
BenchmarkForSlice-4 5000000 280 ns/op
BenchmarkRangeForSlice-4 5000000 277 ns/op
複製程式碼
恩,和我們想的一樣,效能上來了,和常規的for迴圈持平了。原因就是我們通過_
捨棄了元素的複製,然後通過s[i]
獲取迭代的元素,既提高了效能,又達到了目的。
Map 遍歷
對於Map來說,我們並不能使用for i:=0;i<N;i++
的方式,當然如果你有全部的key
元素列表除外,所以大部分情況下我們都是使用for range
的方式。
func RangeForMap1(m map[int]string) {
for k, v := range m {
_, _ = k, v
}
}
const N = 1000
func initMap() map[int]string {
m := make(map[int]string, N)
for i := 0; i < N; i++ {
m[i] = fmt.Sprint("www.flysnow.org",i)
}
return m
}
func BenchmarkRangeForMap1(b *testing.B) {
m:=initMap()
b.ResetTimer()
for i := 0; i < b.N; i++ {
RangeForMap1(m)
}
}
複製程式碼
本文為原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉我的原創說明」歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。
以上示例是map遍歷的函式以及benchmark測試,我都寫在一起了,執行測試看一下效果。
BenchmarkForSlice-8 5000000 298 ns/op
BenchmarkRangeForSlice-8 3000000 475 ns/op
BenchmarkRangeForMap1-8 100000 14531 ns/op
複製程式碼
相比 Slice 來說,Map的遍歷的效能更差,可以說是慘不忍睹。好,我們開始下優化,思路也是減少值得拷貝。測試中的RangeForSlice也慢的原因是我把RangeForSlice還原成了值得拷貝,以便於對比效能。
func RangeForMap2(m map[int]string) {
for k, _ := range m {
_, _ = k, m[k]
}
}
func BenchmarkRangeForMap2(b *testing.B) {
m := initMap()
b.ResetTimer()
for i := 0; i < b.N; i++ {
RangeForMap2(m)
}
}
複製程式碼
再次執行下效能測試看下效果。
BenchmarkForSlice-8 5000000 298 ns/op
BenchmarkRangeForSlice-8 3000000 475 ns/op
BenchmarkRangeForMap1-8 100000 14531 ns/op
BenchmarkRangeForMap2-8 100000 23199 ns/op
複製程式碼
額,是不是發現點不對,方法BenchmarkRangeForMap2
的效能明顯下降了,這個可以從每次操作的耗時看出來(雖然效能測試秒執行的次數還是一樣)。和我們上面測試的Slice不一樣,這次不止沒有提升,反而下降了。
繼續修改Map2
函式的實現為:
func RangeForMap2(m map[int]Person) {
for range m {
}
}
複製程式碼
什麼都不做,只迭代,再次執行效能測試。
BenchmarkForSlice-8 5000000 301 ns/op
BenchmarkRangeForSlice-8 3000000 478 ns/op
BenchmarkRangeForMap1-8 100000 14822 ns/op
BenchmarkRangeForMap2-8 100000 14215 ns/op
複製程式碼
*我們驚奇的發現,什麼都不做,和獲取K-V
值的操作效能是一樣的,和Slice完全不一樣,不是說 for range
值拷貝損耗效能呢?都哪去了?大家猜一猜,可以結合下一節的原理實現
for range 原理
通過檢視github.com/golang/gofr…原始碼,我們可以發現for range
的實現是:
// Arrange to do a loop appropriate for the type. We will produce
// for INIT ; COND ; POST {
// ITER_INIT
// INDEX = INDEX_TEMP
// VALUE = VALUE_TEMP // If there is a value
// original statements
// }
複製程式碼
並且對於Slice,Map等各有具體不同的編譯實現,我們先看看for range slice
的具體實現
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
複製程式碼
先是對要遍歷的 Slice 做一個拷貝,獲取長度大小,然後使用常規for
迴圈進行遍歷,並且返回值的拷貝。
再看看for range map
的具體實現:
// The loop we generate:
// var hiter map_iteration_struct
// for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
// index_temp = *hiter.key
// value_temp = *hiter.val
// index = index_temp
// value = value_temp
// original body
// }
複製程式碼
也是先對map
進行了初始化,因為map
是*hashmap
,所以這裡其實是一個*hashmap
指標的拷貝。
結合著這兩個具體的for range
編譯器實現,可以看看為什麼for range slice
的_
優化方式有用,而for range map
的方式沒用呢?歡迎大家留言回答。
本文為原創文章,轉載註明出處,「總有爛人抓取文章的時候還去掉我的原創說明」歡迎掃碼關注公眾號
flysnow_org
或者網站www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註**……&*¥」覺得好的話,順手分享到朋友圈吧,感謝支援。