Go-For Range 效能研究

林堯彬發表於2020-04-04

      文章轉載地址:https://www.flysnow.org/2018/10/20/golang-for-range-slice-map.html

      如果我們要遍歷某個陣列,Map 集合、Slice 切片等,Go 語言(Golang) 為我們提供了比較好的 For Range 方式。

range 是一個關鍵字, 表示範圍,和 for 配合使用可以迭代 陣列、Map、Slice等集合,用法比較簡潔,那麼,這種

迭代方式和 for i=0;i<N;i++ 對比,效能怎麼樣呢?下面通過 Go 的基準測試對比一下兩者的效能

      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

  下面再看看 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 迴圈對比

       比如對於 Slice 切片,我們有兩種迭代方式:一種是常規的for i:=0;i<N;i++的方式;一種是for range的方式,如下示例:

package main_test

import "testing"

const N  = 1000

// 常規 for 迭代 slice
func ForSlice(s []string) {
	len := len(s)
	for i := 0; i < len; i++ {
		_, _ = i,s[i]
	}
}

// for range 迭代 slice
func RangeForSlice(s []string) {
	for i, v := range s {
		_, _ = i, v
	}
}

// 初始化 slice
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)
	}
}

  輸出結果如下:

goos: windows
goarch: amd64
BenchmarkForSlice-8              5000000               303 ns/op
BenchmarkRangeForSlice-8         3000000               512 ns/op
PASS
ok      _/E_/GoProject/development/src  4.692s

  從上面的輸出結果可以看到,常規的 For 迴圈的效能更高。主要是因為 for range 是每次對迴圈元素的拷貝,而

for 迴圈,它獲取集合內元素是通過 s[i],這種索引指標引用的方式,要比拷貝效能高得多

      那麼既然是元素拷貝的問題,我們在使用 range 方式迭代 slice 時候的目的也是為了獲取元素,現在換一種方式實現 for range:

// for range 迭代 slice
func RangeForSlice(s []string) {
	for i, _ := range s {
		_, _ = i, s[i]
	}
}

  輸出結果:

goos: windows
goarch: amd64
BenchmarkForSlice-8              5000000               303 ns/op
BenchmarkRangeForSlice-8         5000000               308 ns/op
PASS
ok      _/E_/GoProject/development/src  4.218s

  結果和常規的 for 迴圈一樣。原因是我們通過 _ 捨棄了元素的複製,然後通過 s[i] 方式獲取迭代的元素

       Map 遍歷

       對於 map 來說,我們並不能使用 for i=0;i<N;i++ 的方式,大部分我們使用 for range 的方式:

package main_test

import (
	"fmt"
	"testing"
)

const N  = 1000

// for range For map
func RangeForMap1(m map[int]string) {
	for k, v := range m{
		_,_ = k,v
	}
}

// 初始化 map
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)
	}
}

  執行結果如下:

goos: windows
goarch: amd64
BenchmarkRangeForMap1-8           100000             14535 ns/op
PASS
ok      _/E_/GoProject/development/src  2.333s

  相比較 slice,Map 遍歷的效能更差。現在,我們使用上面優化遍歷 slice 的方式優化遍歷 map,減少值拷貝,如下示例:

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

  執行結果如下:

goos: windows
goarch: amd64
BenchmarkRangeForMap1-8           100000             14290 ns/op
BenchmarkRangeForMap2-8           100000             22240 ns/op
PASS
ok      _/E_/GoProject/development/src  4.929s

  我們看到,優化後的結果效能明顯下降了,這和我們上面測試 slice 不一樣,這次沒有提升反而下降了

For Range 原理

        range for slice:

  // The loop we generate:
  //   len_temp := len(range)
  //   range_temp := range
  //   for index_temp = 0; index_temp < len_temp; index_temp++ {
  //           value_temp = range_temp[index_temp]
  //           index = index_temp
  //           value = value_temp
  //           original body
  //   }

  遍歷 slice 前先是對要遍歷的 slice 做一個拷貝,然後獲取 slice 的長度作為迴圈次數,迴圈體中每次迴圈

會先獲取元素值,我們還可以看到遍歷過程中每次迭代都會對 index 和 value 進行賦值,如果資料量比較大或

者 value 為 string 時,對 value 的賦值操作可能是多餘的,所以在上面我們使用 range 遍歷 slice 的時候,可以

忽略 value,使用 slice[index] 的方式提升效能

       range for 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
  //   }

  看上面的實現方式,結合我們使用 for range slice 的 _ 優化方式,我們可以看到看似減少了一次賦值操作,但

是通過 key 查詢 value 的效能消耗高於賦值消耗,這就是為什麼優化沒有起到作用 

轉載於:https://www.cnblogs.com/leeyongbard/p/10394820.html

相關文章