起源
最近在使用 Go 二刷 LeetCode
第一題,兩數之和。解題時使用遍歷求解,偶然發現使用 for
和 range
的 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
}
}
結果如下:
竟然沒有差別。難道結論就是兩者沒有任何區別嗎?我們再來驗證下複雜點的型別
深入
我們看下對 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 的效能幾乎是一樣的。
items
的每一個元素的型別是一個結構體型別Item
,Item
由兩個欄位構成,一個型別是 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
}
}
結果如下:
可以看到,幾乎沒有差別
其實 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 協議》,轉載必須註明作者和本文連結