Golang for Range 探究
前言
今天在用 Golang
写 TCP
长链接的时候,在做测试的时候 for range
有在结构包的时候有性能问题。所以就细化探究了一下。
复现
假设有一个 Packet
类型([1024]byte
)的数据包,我们从网络中获取到一个 [1024]Packet
的数据包,我们进行遍历
type Packet [1024]byte
func BenchmarkForStruct_test(b *testing.B) {
var items [1024]Packet
var result Packet
for i := 0; i < b.N; i++ {
for k := 0; k < len(items); k++ {
result = items[k]
}
}
_ = result
}
func BenchmarkRangeStruct_test(b *testing.B) {
var items [1024]Packet
var result Packet
for i := 0; i < b.N; i++ {
for _, item := range items {
result = item
}
}
_ = result
}
执行 go test -v -bench=. -benchmem -count=4
输出
BenchmarkForStruct_test
BenchmarkForStruct_test-8 1811629 621.3 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1872436 648.8 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1923476 639.4 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1880752 621.9 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test
BenchmarkRangeStruct_test-8 18015 64716 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 18375 64877 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 17505 63145 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 18955 64899 ns/op 0 B/op 0 allocs/op
从上面输出可以看出,for-i
模式 比 for-range
模式快了近90倍,
探究
我找到 golang
的实现 range
的 statements.cc
- range Array Slice 遍历过程
void
For_range_statement::lower_range_array
// 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
// }
void
For_range_statement::lower_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
// }
- range Map 的遍历过程
void
For_range_statement::lower_range_map
// The runtime uses a struct to handle ranges over a map. The
// struct is built by Map_type::hiter_type for a specific map type.
// 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
// }
上面可以看出, slice
和 array
遍历过程相似, 以 长度
作为循环次数,在循环体中会先得到元素的值,如果 for-range
接收 index
和 value
,则对其进行赋值。
遍历 map
中没有指定循环次数,所以循环体中遍历和遍历slice相似,唯一的不同是,map
底层是 hash
,在遍历过程中插入的数据,是随机到哈希桶的,未必会遍历的到。
而 slice
遍历长度固定的,如果中间插入新的,一定遍历不到的。
从这时候可以看出,for-range
在获取值的时候,这时候产生了数据拷贝(值拷贝)。
这时候可以看出,因为 for-range
模式下产生了 数据拷贝 而导致的性能会慢,而 for-i
模式下是 索引指针
的操作。
扩展验证
看如下的代码
func BenchmarkForR_test(b *testing.B) {
var items [1024]byte
var result byte
for i := 0; i < b.N; i++ {
for k := 0; k < len(items); k++ {
result = items[k]
}
}
_ = result
}
func BenchmarkRangeR_test(b *testing.B) {
var items [1024]byte
var result byte
for i := 0; i < b.N; i++ {
for _, item := range items {
result = item
}
}
_ = result
}
我们直接遍历 [1024]byte
, 执行输出
BenchmarkForR_test
BenchmarkForR_test-8 3783114 322.7 ns/op 0 B/op 0 allocs/op
BenchmarkForR_test-8 3791704 309.1 ns/op 0 B/op 0 allocs/op
BenchmarkForR_test-8 3713178 319.8 ns/op 0 B/op 0 allocs/op
BenchmarkForR_test-8 3924511 317.9 ns/op 0 B/op 0 allocs/op
BenchmarkRangeR_test
BenchmarkRangeR_test-8 3671839 315.6 ns/op 0 B/op 0 allocs/op
BenchmarkRangeR_test-8 4002594 312.7 ns/op 0 B/op 0 allocs/op
BenchmarkRangeR_test-8 3779241 315.7 ns/op 0 B/op 0 allocs/op
BenchmarkRangeR_test-8 3885084 321.6 ns/op 0 B/op 0 allocs/op
发现在 byte
类型下基本一致,这是因为是基本类型,都是 值传递 模式,也就是说基本一致可以解释的。
从这也说明了,大部分存在 for-range
性能问题的,针对的遍历类型肯定说是 struct 类型
,
Optimize for-range
struct
看到现在,既然 for-range
在 遍历的对象是struct
类型的时候 ,因为值拷贝的导致的性能问题,那我们选择对其进行优化. 让其不进行值拷贝
,舍弃元素的复制
func BenchmarkRangeStructOptimize_test(b *testing.B) {
var items [1024]Packet
var result Packet
for i := 0; i < b.N; i++ {
for k, _ := range items {
result = items[k]
}
}
_ = result
}
可以看到 我们通过 _ 来丢弃值的复制,只通过索引去取的元素。
执行一下 go test -v -bench=. -benchmem -count=4
输出
BenchmarkForStruct_test
BenchmarkForStruct_test-8 1887973 592.3 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1808815 639.3 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1873707 625.8 ns/op 0 B/op 0 allocs/op
BenchmarkForStruct_test-8 1871689 642.7 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test
BenchmarkRangeStruct_test-8 17035 77642 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 17138 69238 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 16122 70635 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct_test-8 16825 69888 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStructOptimize_test
BenchmarkRangeStructOptimize_test-8 3734373 301.0 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStructOptimize_test-8 3948823 327.5 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStructOptimize_test-8 3780664 317.1 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStructOptimize_test-8 3806779 313.4 ns/op 0 B/op 0 allocs/op
可以看出,优化完的性能比 for-i
在当前环境下更优。
上面代码实现在 github 代码
总结
在使用 for-range
遍历类型为 struct
类型(slice
或 array
)的时候,优先用基本的 for-i
模式,确实要使用 for-range
的话,使用 _
将 值拷贝
给舍弃掉。
Ref: