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:

comments powered by Disqus