$ go tool compile -m main.go # 或者 $ go build -gcflags='-m -l' main.go• 反汇编
$ go tool compile -S main.go
package main func main() { data := []interface{}{100, 200} data[0] = 100 } $ go tool compile -m main.go输出如下
main.go:3:6: can inline main main.go:4:23: []interface {}{...} does not escape main.go:4:24: 100 does not escape // 未发生逃逸 main.go:4:29: 200 does not escape // 未发生逃逸 main.go:5:2: 100 escapes to heap // 发生逃逸
package performance import "testing" const size = 1024 // 堆代码 duidaima.com func genSeqNumbers() interface{} { var res [size]int for i := 0; i < len(res); i++ { if i <= 1 { res[i] = 1 continue } res[i] = res[i-1] + res[i-2] } return res } func Benchmark_Compare(b *testing.B) { for i := 0; i < b.N; i++ { _ = genSeqNumbers() } }运行测试,并将基准测试结果写入文件:
# 运行 100000 次, 统计内存分配 $ go test -run='^$' -bench=. -count=1 -benchtime=100000x -benchmem > slow.txt
package performance import "testing" const size = 1024 func genSeqNumbers() [1024]int { var res [size]int for i := 0; i < len(res); i++ { if i <= 1 { res[i] = 1 continue } res[i] = res[i-1] + res[i-2] } return res } func Benchmark_Compare(b *testing.B) { for i := 0; i < b.N; i++ { _ = genSeqNumbers() } }运行测试,并将基准测试结果写入文件:
# 运行 100000 次, 统计内存分配 $ go test -run='^$' -bench=. -count=1 -benchtime=100000x -benchmem > fast.txt使用 benchstat 比较差异
$ benchstat -alpha=100 fast.txt slow.txt输出如下
name old time/op new time/op delta _Compare-8 1.44µs ± 0% 3.22µs ± 0% +123.40% (p=1.000 n=1+1) name old alloc/op new alloc/op delta _Compare-8 0.00B 8192.00B ± 0% +Inf% (p=1.000 n=1+1) name old allocs/op new allocs/op delta _Compare-8 0.00 1.00 ± 0% +Inf% (p=1.000 n=1+1)从输出的结果中可以看到,通过使用确定的数据类型,运行时间提升了 1 倍多, 内存分配量和内存分配次数降为 0。高性能 Tips: 在 hot path 上要尽可能返回具体的数据类型。
package main func main() { weekdays := 7 data := make(map[string][]int) data["weeks"] = make([]int, weekdays) data["weeks"] = append(data["weeks"], []int{0, 1, 2, 3, 4, 5, 6}...) } $ go tool compile -m main.go输出如下 (发生逃逸)
main.go:3:6: can inline main main.go:5:14: make(map[string][]int) does not escape main.go:6:22: make([]int, weekdays) escapes to heap main.go:7:45: []int{...} does not escape避免逃逸方案
package main func main() { data := make(map[string][7]int) data["weeks"] = [...]int{0, 1, 2, 3, 4, 5, 6} } $ go tool compile -m main.go输出如下 (没有发生逃逸)
main.go:3:6: can inline main main.go:4:14: make(map[string][7]int) does not escape指针
package main func main() { n := 10 data := make([]*int, 1) data[0] = &n } $ go tool compile -m main.go输出如下 (发生逃逸)
main.go:3:6: can inline main main.go:4:2: moved to heap: n main.go:5:14: make([]*int, 1) does not escapeinterface{}
package main func main() { data := make(map[interface{}]interface{}) data[100] = 200 } $ go tool compile -m main.go输出如下 (发生逃逸)
main.go:3:6: can inline main main.go:4:14: make(map[interface {}]interface {}) does not escape main.go:5:2: 100 escapes to heap main.go:5:2: 200 escapes to heap
package main import "math/rand" func foo(argVal int) *int { var fooVal1 = 11 var fooVal2 = 12 var fooVal3 = 13 var fooVal4 = 14 var fooVal5 = 15 // 堆代码 duidaima.com // 循环是防止编译器将 foo 函数优化为 inline // 如果不用随机数指定循环次数,也可能被编译器优化为 inline // 如果是内联函数,main 调用 foo 将是原地展开 // 那么 fooVal1 ... fooVal5 相当于 main 作用域的变量 // 即使 fooVal3 发生逃逸,地址与其他几个变量也是连续的 n := rand.Intn(5) + 1 for i := 0; i < n; i++ { println(&argVal, &fooVal1, &fooVal2, &fooVal3, &fooVal4, &fooVal5) } return &fooVal3 } func main() { mainVal := foo(1) println(*mainVal, mainVal) }运行代码
$ go run main.go输出如下 (发生逃逸)
0xc000114f58 0xc000114f38 0xc000114f30 0xc000120000 0xc000114f28 0xc000114f20 0xc000114f58 0xc000114f38 0xc000114f30 0xc000120000 0xc000114f28 0xc000114f20 13 0xc000120000通过输出的结果可以看到,变量 fooVal3 的地址明显与其他变量地址不是连续的。
结果分析
$ go tool compile -m main.go输出如下 (发生逃逸)
main.go:16:16: inlining call to rand.Intn main.go:24:6: can inline main main.go:8:6: **moved to heap: fooVal3** # 查看逃逸分析详情 $ go build -gcflags='-m -l' main.go # 输出如下 (发生逃逸) ./main.go:5:6: cannot inline foo: function too complex: cost 124 exceeds budget 80 ... ./main.go:8:6: fooVal3 escapes to heap: ... ./main.go:8:6: **moved to heap: fooVal3** # 或者 $ go tool compile -S main.go | grep "runtime.newobject" # 输出如下 0x0036 00054 (**main.go:8**) CALL runtime.newobject(SB) rel 55+4 t=7 runtime.newobject+0 # main.go 第 8 行正好是 var fooVal3 = 13通道
package main func main() { ch := make(chan string) s := "hello world" go func() { ch <- s }() <-ch } $ go tool compile -m main.go # 输出如下 (发生逃逸) main.go:19:5: can inline main.func1 main.go:19:5: func literal escapes to heap闭包
package main func inc() func() int { n := 0 return func() int { n++ return n } } func main() { in := inc() println(in()) // 1 println(in()) // 2 } $ go tool compile -m main.go # 输出如下 (发生逃逸) main.go:3:6: can inline inc main.go:5:9: can inline inc.func1 main.go:12:11: inlining call to inc main.go:5:9: can inline main.func1 main.go:13:12: inlining call to main.func1 main.go:14:12: inlining call to main.func1 main.go:4:2: **moved to heap: n** main.go:5:9: func literal escapes to heap main.go:12:11: func literal does not escapeinc() 函数返回一个闭包函数,闭包函数内部访问了外部变量 n, 形成了引用关系,那么外部变量 n 将会一直存在,直到 inc() 函数被销毁, 所以最终外部变量 n 被分配到了 堆 上。
package main import "math/rand" func generate8192() { nums := make([]int, 8192) // = 64KB for i := 0; i < 8192; i++ { nums[i] = rand.Int() } } func generate8193() { nums := make([]int, 8193) // < 64KB for i := 0; i < 8193; i++ { nums[i] = rand.Int() } } func generate(n int) { nums := make([]int, n) // 不确定大小 for i := 0; i < n; i++ { nums[i] = rand.Int() } } func main() { generate8192() generate8193() generate(1) } $ go tool compile -m main.go # 输出如下 (发生逃逸) main.go:6:14: make([]int, 8192) does not escape main.go:13:14: make([]int, 8193) escapes to heap main.go:20:14: make([]int, n) escapes to heap从输出的结果中可以看到,make([]int, 8192) 没有发生逃逸,make([]int, 8193) 和 make([]int, n) 逃逸到 堆 上, 说明当切片占用内存超过一定大小,或无法确定当前切片长度时,对象将分配到 堆 上。