Go进阶(9): For Range 性能研究
遍歷數組,map集合,Slice切片等,Go提供比較好用的For Range方式。range是一個關鍵字,表示范圍,和for配合使用可以迭代數組,map等集合。用法簡潔,而且map、channel等也都是用for range的方式,所以在編碼中我們使用for range進行循環迭代是最多的。對于這種最常使用的迭代,尤其是和for i=0;i<N;i++對比,性能怎么樣?需要深入了解。
1. 基本用法
package main import "fmt" func main() {ages:=map[string]int{"張三":15,"李四":20,"王武":36}for name,age:=range ages{fmt.Println(name,age)} }>> 張三 15 >> 李四 20 >> 王武 36在使用for range迭代map的時候,返回的第一個變量是key,第二個變量是value.需要注意的是,for range map返回的K-V鍵值對順序是不固定的,是隨機的.
2. 與常規的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 = 1000func 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]獲取迭代的元素,既提高了性能,又達到了目的。
3. map的遍歷只能使用range
對于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 = 1000func 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)} } 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這件事說明,這不僅僅是值拷貝的消耗。
4. for range原理
https://github.com/golang/gofrontend
對于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的slice是可以優化的,而map幾乎是不可能的。
總結
以上是生活随笔為你收集整理的Go进阶(9): For Range 性能研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [java进阶]4.关键字throws和
- 下一篇: 内存经销商穷困潦倒 七元午饭都赊账