我们选择 go 语言的一个重要原因是,它有非常高的性能。但是它反射的性能却一直为人所诟病,本篇文章就来看看 go 反射的性能问题。

go 的性能测试

Benchmarkb.N
go test -bench=. reflect_test.go

说明:

*_test.goBenchmark**testing.Bb.ReportAllocs()b.NNew()

go 里面很多优化都致力于减少内存分配,减少内存分配很多情况下都可以提高性能。

输出:

BenchmarkNew-20    1000000000    0.1286 ns/op   0 B/op   0 allocs/op

输出说明:

BenchmarkNew-20BenchmarkNew-2010000000000.1286 ns/op0 B/op0 allocs/op

go 反射慢的原因

动态语言的灵活性是以牺牲性能为代价的,go 语言也不例外,go 的 interface{} 提供了一定的灵活性,但是处理 interface{} 的时候就要有一些性能上的损耗了。

interface{}interface{}interface{}

go interface{} 带来的灵活性

interface{}interface{}int64

说明:

convert()interface{}int64uint*add()int64

相比之下,如果是确定的类型,我们根本不需要判断类型,直接相加就可以了:

我们可以通过以下的 benchmark 来对比一下:

结果:

BenchmarkAdd-12         179697526                6.667 ns/op           0 B/op          0 allocs/op
BenchmarkAdd1-12        1000000000               0.2353 ns/op          0 B/op          0 allocs/op

add()add1()

go 灵活性的代价(慢的原因)

interface{}
FieldFieldByNameMethodMethodByNameFieldMethodFieldByNameMethodByNameintinterface{}
FieldByName

慢是相对的

从上面的例子中,我们发现 go 的反射好像慢到了让人无法忍受的地步,然后就有人提出了一些解决方案, 比如:通过代码生成的方式避免运行时的反射操作,从而提高性能。比如 easyjson

但是这类方案都会让代码变得繁杂起来。我们需要权衡之后再做决定。为什么呢?因为反射虽然慢,但我们要知道的是,如果我们的应用中有网络调用,任何一次网络调用的时间往往都不会少于 1ms,而这 1ms 足够 go 做很多次反射操作了。这给我们什么启示呢?如果我们不是做中间件或者是做一些高性能的服务,而是做一些 web 应用,那么我们可以考虑一下性能瓶颈是不是在反射这里,如果是,那么我们就可以考虑一下代码生成的方式来提高性能,如果不是,那么我们真的需要牺牲代码的可维护性、可读性来提高反射的性能吗?优化几个慢查询带来的收益是不是更高呢?

go 反射性能优化

如果可以的话,最好的优化就是不要用反射

通过代码生成的方式避免序列化和反序列化时的反射操作

easyjson
easyjsoneasyjson
person_easyjson.goMarshalJSONUnmarshalJSONjson.Marshaljson.Unmarshal
Person

性能差距:

goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin/c/easy
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkJson
BenchmarkJson-12            3680560          305.9 ns/op      152 B/op         2 allocs/op
BenchmarkEasyJson
BenchmarkEasyJson-12       16834758           71.37 ns/op         128 B/op         1 allocs/op

easyjson

反射结果缓存

这种方法适用于需要根据名称查找结构体字段或者查找方法的场景。

PersonM1M2M3M4M5reflect

这是很容易想到的办法,但是性能如何呢?通过性能测试,我们可以看到,这种方式的性能是非常差的:

结果:

BenchmarkMethodByName-12         5051679               237.1 ns/op           120 B/op          3 allocs/op

相比之下,我们如果使用索引来获取其中的方法的话,性能会好很多:

结果:

BenchmarkMethod-12              200091475                5.958 ns/op           0 B/op          0 allocs/op

MethodMethodByNameMethodByName

这里需要通过 reflect.Type 的 MethodByName 来获取反射的方法对象。

性能测试:

结果:

跟这个例子类似的是 Field/FieldByName 方法,可以采用同样的优化方式。这个可能是更加常见的操作,反序列化可能需要通过字段名查找字段,然后进行赋值。

使用类型断言代替反射

在实际使用中,如果只是需要进行一些简单的类型判断的话,比如判断是否实现某一个接口,那么可以使用类型断言来实现:

结果:

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkReflectCall-12          6906339               173.1 ns/op
BenchmarkAssert-12              171741784                6.922 ns/op

在这个例子中,我们就算使用了缓存版本的反射,性能也跟类型断言差了将近 25 倍。

因此,在我们使用反射之前,我们需要先考虑一下是否可以通过类型断言来实现,如果可以的话,那么就不需要使用反射了。

总结

go test -bench=.Benchmark*
b.ReportAllocs()

反射虽然慢,但是也带来了一定的灵活性,它的慢主要由以下几个方面的原因造成的:

FieldByNameMethodByName
FieldFieldByName

如果可以的话,尽量不使用反射就是最好的优化。

反射的一些性能优化方式有如下几种(不完全,需要根据实际情况做优化):

  • 使用生成代码的方式,生成特定的序列化和反序列化方法,这样就可以避免反射的开销。
  • 将第一次反射拿到的结果缓存起来,这样如果后续需要反射的话,就可以直接使用缓存的结果,避免反射的开销。(空间换时间
  • 如果只是需要进行简单的类型判断,可以先考虑一下类型断言能不能实现我们想要的效果,它相比反射的开销要小很多。

反射是一个很庞大的话题,这里只是简单的介绍了一小部分反射的性能问题,讨论了一些可行的优化方案,但是每个人使用反射的场景都不一样,所以需要根据实际情况来做优化。