Go語言性能優化- For Range 性能研究

若是咱們要遍歷某個數組,Map集合,Slice切片等,Go語言(Golang)爲咱們提供了比較好用的For Range方式。range是一個關鍵字,表示範圍,和for配合使用能夠迭代數組,Map等集合。它的用法簡潔,並且map、channel等也都是用for range的方式,因此在編碼中咱們使用for range進行循環迭代是最多的。對於這種最常使用的迭代,尤爲是和for i=0;i<N;i++對比,性能怎麼樣?咱們進行下示例分析,讓咱們對for range循環有個更深的理解,便於咱們寫出性能更高的程序。html

基本用法

for range的使用很是簡單,這裏演示下兩種集合類型的使用。git

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 切片中的內容,因此咱們打印出來:github

0 10
1 20
2 30

關於Go語言 Slice 切片的,能夠參考我之前寫的這篇 Go語言實戰筆記(五)| Go 切片golang

下面再看看map(字典)的for range使用示例。api

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,也就是咱們例子中對應的nameages。咱們運行程序看看輸出結果。數組

張三 15
李四 20
王武 36

這裏須要注意的是,for range map返回的K-V鍵值對順序是不固定的,是隨機的,此次多是張三-15第一個出現,下一次運行多是王武-36第一個被打印了。
關於Map更詳細的能夠參考我之前的一篇文章 Go語言實戰筆記(六)| Go Mapapp

常規for循環對比

好比對於 Slice 切片,咱們有兩種迭代方式:一種是常規的for i:=0;i<N;i++的方式;一種是for range的方式,下面咱們看看兩種迭代的性能。frontend

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 查看性能測試結果。oop

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)
    }
}

http://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 原理

經過查看https://github.com/golang/gofrontend源代碼,咱們能夠發現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或者網站 http://www.flysnow.org/,第一時間看後續精彩文章。「防爛人備註 *……&¥」以爲好的話,順手分享到朋友圈吧,感謝支持。

掃碼關注

相關文章
相關標籤/搜索