for & range 效能對比

attitude發表於2021-05-20

起源

最近在使用 Go 二刷 LeetCode
第一題,兩數之和。解題時使用遍歷求解,偶然發現使用 forrange的 beats不一致,本著深入研究(啥也不懂)的精神,就想對比下兩者的效能如何。
本文參考極客兔兔大佬的原創

探索

既然要對比,那就使用資料說話。
GO test 命令不但可以做單元測試,還支援 bench 進行效能對比。具體操作自行研究,本文就不做深究了。
基本命令:

go test -bench .

首先驗證下 int型別的遍歷

func genIntSlice(n int) []int {
    rand.Seed(time.Now().UnixNano())
    nums := make([]int, 0, n)
    for i := 0; i < n; i++ {
        nums = append(nums, rand.Int())
    }
    return nums
}

func BenchmarkForIntSlice(b *testing.B) {
    nums := genIntSlice(1024 * 1024)
    for i := 0; i < b.N; i++ {
        length := len(nums)
        var tmp int
        for k := 0; k < length; k++ {
            tmp = nums[k]
        }
        _ = tmp
    }
}

func BenchmarkRangeIntSlice(b *testing.B) {
    nums := genIntSlice(1024 * 1024)
    for i := 0; i < b.N; i++ {
        var tmp int
        for _, num := range nums {
            tmp = num
        }
        _ = tmp
    }
}

結果如下:

for & range 效能對比
竟然沒有差別。難道結論就是兩者沒有任何區別嗎?我們再來驗證下複雜點的型別

深入

我們看下對 struct 的遍歷有什麼區別

type Item struct {
    id int
    val [4096]byte
}

func BenchmarkForStruct(b *testing.B) {
    var items [1024]Item
    for i := 0; i < b.N; i++ {
        length := len(items)
        var tmp int
        for k := 0; k < length; k++ {
            tmp = items[k].id
        }
        _ = tmp
    }
}

func BenchmarkRangeIndexStruct(b *testing.B) {
    var items [1024]Item
    for i := 0; i < b.N; i++ {
        var tmp int
        for k := range items {
            tmp = items[k].id
        }
        _ = tmp
    }
}

func BenchmarkRangeStruct(b *testing.B) {
    var items [1024]Item
    for i := 0; i < b.N; i++ {
        var tmp int
        for _, item := range items {
            tmp = item.id
        }
        _ = tmp
    }
}

結果如下:

for & range 效能對比

很明顯的區別對不對!

  • 僅遍歷下標的情況下,for 和 range 的效能幾乎是一樣的。
  • items 的每一個元素的型別是一個結構體型別 ItemItem 由兩個欄位構成,一個型別是 int,一個是型別是 [4096]byte,也就是說每個 Item 例項需要申請約 4KB 的記憶體。
  • 在這個例子中,for 的效能大約是 range (同時遍歷下標和值) 的 2000 倍。

繼續深入一下,我們如果遍歷指標型別呢?

func generateItems(n int) []*Item {
    items := make([]*Item, 0, n)
    for i := 0; i < n; i++ {
        items = append(items, &Item{id: i})
    }
    return items
}

func BenchmarkForPointer(b *testing.B) {
    items := generateItems(1024)
    for i := 0; i < b.N; i++ {
        length := len(items)
        var tmp int
        for k := 0; k < length; k++ {
            tmp = items[k].id
        }
        _ = tmp
    }
}

func BenchmarkRangePointer(b *testing.B) {
    items := generateItems(1024)
    for i := 0; i < b.N; i++ {
        var tmp int
        for _, item := range items {
            tmp = item.id
        }
        _ = tmp
    }
}

結果如下:

for & range 效能對比

可以看到,幾乎沒有差別

其實 range在迭代過程中返回的是迭代值的拷貝,這個也可以簡單驗證下:

persons := []struct{ no int }{{no: 1}, {no: 2}, {no: 3}}
for _, s := range persons {
    s.no += 10
}
for i := 0; i < len(persons); i++ {
    persons[i].no += 100
}
fmt.Println(persons) // [{101} {102} {103}]

最終

range 在迭代過程中返回的是迭代值的拷貝,如果每次迭代的元素的記憶體佔用很低,那麼 for 和 range 的效能幾乎是一樣,例如 []int。但是如果迭代的元素記憶體佔用較高,例如一個包含很多屬性的 struct 結構體,那麼 for 的效能將顯著地高於 range,有時候甚至會有上千倍的效能差異。對於這種場景,建議使用 for,如果使用 range,建議只迭代下標,通過下標訪問迭代值,這種使用方式和 for 就沒有區別了。如果想使用 range 同時迭代下標和值,則需要將切片/陣列的元素改為指標,才能不影響效能。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
attitudefx

相關文章