for range 性能
range 会拷贝遍历对象。
range 实现原理
slice
遍历 slice 前会先获取 slice 的长度 len_temp 作为循环次数。获取被遍历对象的拷贝,作为 for_temp。
循环体中,每次循环会先获取元素值 value_temp,以及下标 index_temp 进行赋值。
注意,在循环开始前就已经确定了循环次数、并且已经将遍历对象保存了下来。
1 | // The loop we generate: |
map
遍历 map 时没有指定循环次数,只是移动遍历指针 hiter,当遍历指针的 key 为 nil 时则停止循环。
map 底层使用 hash 表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。
1 | // The loop we generate: |
channel
channe l遍历是依次从 channel 中读取数据,读取前是不知道里面有多少个元素的。
如果 channel 中没有元素,则会阻塞等待,如果 channel 已被关闭,则会解除阻塞并退出循环。
1 | // The loop we generate: |
性能分析
因为 range 会拷贝遍历对象,所以 for 和 range 的性能比较如下:
- 每次迭代的元素的内存占用很低,那么 for 和 range 的性能几乎是一样。
- 如果迭代的元素内存占用较高,那么 for 的性能将显著地高于 range,因为 range 会拷贝迭代对象。可以考虑只迭代下标,这样和 for 的性能是一样的。
- 如果想使用 range 同时迭代下标和值,则可以将迭代元素修改为指针。
反射的性能
反射的性能
创建对象
使用反射创建对象的步骤如下:
1 | func New(typ Type) Value |
- 在使用反射创建对象时,使用 relect.New 方法返回一个 Value 类型值,该值持有一个指向类型为 typ 的新申请的零值的指针。
- 接着调用 Value.Interface 方法,返回持有的当前值,保存在 interface{} 类型中。
- 最后使用类型断言,将 interface{} 类型转换为目标类型。
对比 x= new(X)
创建对象,以及通过反射 x, _ = reflect.New(XTypeOf).Interface().(*X)
创建对象,其 benchmark 测试如下:
1 | BenchmarkNew-8 26478909 40.9 ns/op |
通过反射创建对象的耗时约为 new
的 1.5 倍,相差不是特别大。
修改字段的值
通过反射获取结构体的字段有两种方式:
- 一种是 FieldByName(根据字段名字)。
- 另一种是 Field(根据下标)。
接着通过 Field.SetXxx 方法就可以修改属性了。
直接修改、通过 Field 方法获取属性修改、通过 FieldByName 获取属性修改的性能测试如下:
1 | BenchmarkSet-8 1000000000 0.302 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 中顺序查找。