Dawn's Blogs

分享技术 记录成长

0%

Go语言高性能编程 (2) 常用数据结构——for range的性能与反射的性能

for range 性能

range 会拷贝遍历对象。

range 实现原理

slice

遍历 slice 前会先获取 slice 的长度 len_temp 作为循环次数。获取被遍历对象的拷贝,作为 for_temp

循环体中,每次循环会先获取元素值 value_temp,以及下标 index_temp 进行赋值。

注意,在循环开始前就已经确定了循环次数、并且已经将遍历对象保存了下来

1
2
3
4
5
6
7
8
9
// 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
// }

map

遍历 map 时没有指定循环次数,只是移动遍历指针 hiter,当遍历指针的 key 为 nil 时则停止循环。

map 底层使用 hash 表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。

1
2
3
4
5
6
7
8
9
// 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
// }

channel

channe l遍历是依次从 channel 中读取数据,读取前是不知道里面有多少个元素的。

如果 channel 中没有元素,则会阻塞等待,如果 channel 已被关闭,则会解除阻塞并退出循环。

1
2
3
4
5
6
7
8
9
// The loop we generate:
// for {
// index_temp, ok_temp = <-range
// if !ok_temp {
// break
// }
// index = index_temp
// original body
// }

性能分析

因为 range 会拷贝遍历对象,所以 for 和 range 的性能比较如下:

  • 每次迭代的元素的内存占用很低那么 for 和 range 的性能几乎是一样
  • 如果迭代的元素内存占用较高那么 for 的性能将显著地高于 range,因为 range 会拷贝迭代对象。可以考虑只迭代下标,这样和 for 的性能是一样的。
  • 如果想使用 range 同时迭代下标和值,则可以将迭代元素修改为指针

反射的性能

反射的性能

创建对象

使用反射创建对象的步骤如下:

1
func New(typ Type) Value
  1. 在使用反射创建对象时,使用 relect.New 方法返回一个 Value 类型值,该值持有一个指向类型为 typ 的新申请的零值的指针
  2. 接着调用 Value.Interface 方法,返回持有的当前值,保存在 interface{} 类型中。
  3. 最后使用类型断言,将 interface{} 类型转换为目标类型。

对比 x= new(X) 创建对象,以及通过反射 x, _ = reflect.New(XTypeOf).Interface().(*X) 创建对象,其 benchmark 测试如下:

1
2
BenchmarkNew-8                  26478909                40.9 ns/op
BenchmarkReflectNew-8 18983700 62.1 ns/op

通过反射创建对象的耗时约为 new 的 1.5 倍,相差不是特别大

修改字段的值

通过反射获取结构体的字段有两种方式:

  • 一种是 FieldByName(根据字段名字)。
  • 另一种是 Field(根据下标)。

接着通过 Field.SetXxx 方法就可以修改属性了。

直接修改、通过 Field 方法获取属性修改、通过 FieldByName 获取属性修改的性能测试如下:

1
2
3
BenchmarkSet-8                          1000000000               0.302 ns/op
BenchmarkReflect_FieldSet-8 33913672 34.5 ns/op
BenchmarkReflect_FieldByNameSet-8 3775234 316 ns/op
  • 可以看到使用反射修改字段值比正常修改的性能降低了非常多(在本例中降低了 100 - 1000 倍)。
  • 使用 FieldByName 又比 Field 性能降低了很多

FieldByName 和 Field 性能差距

FieldByName 为什么比 Field 的效率差这么多呢?首先看看 FieldByName 的调用链条:

1
(v Value) FieldByName -> (t *rtype) FieldByName -> (t *structType) FieldByName

核心在于 (t *structType) FieldByName 方法内,使用 for 循环,逐个字段查找,字段名匹配时返回。

因此,使用 Field 利用下标查询效率为 O(1),而使用 FieldByName 按照字段名字查询的效率为 O(n)

使用说明

避免使用反射

在实际应用过程中,应该尽量避免使用反射,因为反射效率低下

在使用 json 序列化和反序列化时,应该避免使用 Go 语言自带的 json.Marshal 和 json.Unmarshal 方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升

缓存

FieldByName 和 Field 有非常巨大的效率差距,那在实际的应用中,就要避免直接调用 FieldByName

可以将利用 map 将字段名字和下标映射起来,避免在 FieldByName 中顺序查找。