在上一篇《reflect实践与剖析》的文章中,本来打算写的更深入一点,结果发现reflect里面的东西太多了,在一篇文章中很难写全,就草草收尾了。今天谈一谈关于reflect的另一个问题:reflect为什么慢?
先问是不是,再问为什么。我们先来看一下reflect是不是真的慢。
Golang testing
golang提供了一个testing包,使得单元测试、性能测试尤为简单。只要新建一个以_test结尾的文件,然后使用命令go test就可以自动执行文件中的相应测试函数了(单元测试函数以Test开头,性能测试函数以Benchmark开头)。除了单元测试和性能测试,testing还有其他的功能,暂且按住不表。下面主要介绍一下单元测试和性能测试怎么写。
单元测试
比如我们写了一个简单的浮点数除法函数并编写单元测试。
//浮点除法函数: division.gofunc Division(a, b float64) (float64, error) { return a/b, nil}//单元测试: division_test.gofunc TestDivision(t *testing.T) { if i, e := Division(9.3, 3); i!=3.1 || e!= nil { t.Error("Division Normal Test Failed") } else if i, e:= Division(1,0); i!=0 || e.Error()!="divisor can't be 0"{ t.Error("Division divisor=0 test Failed") } else { t.Log("Division Test Failed") }}
执行使用go test filename_test.go,对filename_test.go文件进行测试,如果不加文件,则运行当前目录下的所有已_test结尾的文件。为了便于测试Division函数特意没有考虑除数为0的情况。测试结果如下。
--> gotest $ go test division_test.go--- FAIL: TestDivision (0.00s)division_test.go:19: Division divisor=0 test FailedFAIL
完善Division函数如下,再运行显示ok。
func Division(a, b float64) (float64, error){ if b == 0 { return 0, fmt.Errorf("divisor can't be 0") } return a/b, nil}
性能测试
还是对于上面的Division函数,编写性能测试函数如下。
func BenchmarkDivision(b *testing.B) { for i:=0; i
性能测试的时候需要加一个-bench参数,对于我们这个例子使用go test -bench='.' division_test.go即可,执行结果如下:
PASSBenchmarkDivision-85000000003.07 ns/opok
中间一行的意思是执行了5亿次,每次耗时3.07纳秒。
Reflect Benchmark 测试
我们使用golang testing来做一下reflect的最简单的性能测试。
import ( "testing" "reflect")type Bench struct { A int}func Foo1(b Bench) { _ = Bench{}}func Foo2(x interface{}) { _ = reflect.ValueOf(x)}func BenchmarkFoo1(b *testing.B) { var x Bench for i:=0; i
运行go test -bench='.',结果如下
PASSBenchmarkFoo1-82000000000.47 ns/opBenchmarkFoo2-8300000036.7 ns/op
比较来看差距确实比较大啊,当然如果单纯从时间角度来看感觉还好。我们来看一下reflect的源码,试着分析一下时间都浪费在了什么地方。
func ValueOf(i interface{}) Value { if i == nil { return Value{} } escapes(i)//异常处理,先不用管 return unpackEface(i)}func unpackEface(i interface{}) Value { e := (*emptyInterface)(unsafe.Pointer(&i)) t := e.typ if t == nil { return Value{} } f := flag(t.Kind()) if ifaceIndir(t) { f |= flagIndir } return Value{t, e.word, f}}
ValueOf的工作是将built-in类型或者自定义类型(比如struct)转换成reflect包中的Value Struct,看上去耗时的地方应该就是最后面的内存分配了。为了验证想法,可以做一下CPU Profiling
Golang Profiling
golang提供了一套工具pprof用来做性能分析与优化,主要分为两种:memory使用情况和CPU使用情况。关于pprof的具体使用这里就不细说,后面会把他和gcvis放在一起再说一下。为了做profiling,需要在原程序插入一些代码,主要是做采集用的,代码如下,插入的代码已经标记出来了。
import ( "flag" "fmt" "os" "reflect" "runtime/pprof")type Kltao struct { Ageint}func Foo(k interface{}) { _ = reflect.ValueOf(k)}var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")func main() { flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { fmt.Println(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } for i:=0; i<50000000; i++ { var k Kltao Foo(k) }}
$ go build reflectt.go$ ./reflectt cpuprofile=reflect.prof$ go tool pprof reflectt reflect.profEntering interactive mode (type "help" for command)(pprof) web
看了上图我只想说,这个真不是我想象的那么简单,这个图真是看的脑袋疼。仔细分析之后可以得到一个大概结论,reflect慢主要有两个原因:一是涉及到内存分配以后GC;二是reflect实现里面有大量的枚举,也就是for循环,比如类型之类的。
其他Benchmark Test
上面的测试太简单了,可能会有以偏概全的嫌疑,羡慕提供了一个函数调用的性能测试,profiling图我先自己研究研究。
package testimport ( "testing" "reflect")type Kltao struct {//Name string Age int//B int//C int//D int}func (k Kltao) Foo() { for i := 0; i < 10000; i++ { k.Age++ }}func (k Kltao) Foo1() { for i := 0; i < 10000; i++ { k.Age++ }}func (k Kltao) Foo2() { for i := 0; i < 1000000; i++ { k.Age++ }}func (k Kltao) Foo3() { for i := 0; i < 10000; i++ { k.Age++ }}func SimpleFoo(kltao Kltao) { kltao.Foo1()}func ReflectFoo(k interface{}) { v := reflect.ValueOf(k) foo := v.MethodByName("Foo1") foo.Call(nil)}func BenchmarkSimpleFoo(b *testing.B) { var kltao Kltao for i := 0; i < b.N; i++ { SimpleFoo(kltao) }}func BenchmarkReflectFoo(b *testing.B) { var kltao Kltao for i := 0; i < b.N; i++ { ReflectFoo(kltao) }}
性能测试结果如下。
PASSBenchmarkSimpleFoo-85000003206 ns/opBenchmarkReflectFoo-8500000 3547 ns/op