golang gc/arch 对 benchmark 的影响

2017-12-18 10:52:26来源:作者:人点击

分享

最近在同事提出了一个疑问:
在对一个slice进行遍历时,将for循环条件中的len提出到循环外是否会比golang编译器的优化结果更加好。


即:


func g0(a []int) int {
l := len(a)
for i := 0; i < l; i++ {
}
return 1
}

是否会比


func g1(a []int) int {
for i := 0; i < len(a); i++ {
}
return 1
}

的结果更加优化(目前golang的编译器并不会对这个空循环进行消除)。


那为了证明这个问题,那就上 benchmark 证明啊。


import "testing"
var a = make([]int, 1<<25)
func BenchmarkG0(b *testing.B) {
for i := 0; i < b.N; i++ {
g0(a)
}
}
func BenchmarkG1(b *testing.B) {
for i := 0; i < b.N; i++ {
g1(a)
}
}

然后执行


go test -c .
./len.test -test.bench=. -test.count=2

得到输出结果:


goos: darwin
goarch: amd64
BenchmarkG0-410011784627 ns/op
BenchmarkG0-410011841061 ns/op
BenchmarkG1-410018623122 ns/op
BenchmarkG1-410017790754 ns/op
PASS

果然g0比g1速度快很多,但是这个有点反常识啊,不能这样轻易下定结论。那我们来看看g0的编译结果
是否就比g1优化很多:


我们来执行


go tool objdump ./len.test > main.s

我们来看得到的结果:


TEXT _/test/go/len.g0(SB) /test/go/len/main.go
main.go:4 0x10ef150 488b442410MOVQ 0x10(SP), AX
main.go:4 0x10ef155 31c9XORL CX, CX
main.go:6 0x10ef157 eb03JMP 0x10ef15c
main.go:6 0x10ef159 48ffc1INCQ CX
main.go:6 0x10ef15c 4839c1CMPQ AX, CX
main.go:6 0x10ef15f 7cf8JL 0x10ef159
main.go:8 0x10ef161 48c744242001000000MOVQ $0x1, 0x20(SP)
main.go:8 0x10ef16a c3RET
:-1 0x10ef16b ccINT $0x3
:-1 0x10ef16c ccINT $0x3
:-1 0x10ef16d ccINT $0x3
:-1 0x10ef16e ccINT $0x3
:-1 0x10ef16f ccINT $0x3
TEXT _/test/go/len.g1(SB) /test/go/len/main.go
main.go:120x10ef170 488b442410MOVQ 0x10(SP), AX
main.go:120x10ef175 31c9XORL CX, CX
main.go:130x10ef177 eb03JMP 0x10ef17c
main.go:130x10ef179 48ffc1INCQ CX
main.go:130x10ef17c 4839c1CMPQ AX, CX
main.go:130x10ef17f 7cf8JL 0x10ef179
main.go:150x10ef181 48c744242001000000MOVQ $0x1, 0x20(SP)
main.go:150x10ef18a c3RET
:-1 0x10ef18b ccINT $0x3
:-1 0x10ef18c ccINT $0x3
:-1 0x10ef18d ccINT $0x3
:-1 0x10ef18e ccINT $0x3
:-1 0x10ef18f ccINT $0x3

我们可以看到编译器生成的中间代码完全是相同的,那为什么在运行起来会有不同的结果呢?
那我们就要考虑了,除了代码以外,还有什么会影响代码执行?那就是:


运行环境
runtime

那我们就分别对这两个因素进行验证。


首先是运行环境,我们换到linux上再进行一次验证:


GOOS=linux GOARCH=amd64 go test -c .
## copy to linux
./test.len -test.bench=. -test.count=2

得到输出结果:


goos: linux
goarch: amd64
BenchmarkG0-32 10010824437 ns/op
BenchmarkG0-32 10010743979 ns/op
BenchmarkG1-32 10010740347 ns/op
BenchmarkG1-32 10010898047 ns/op
PASS

在linux上g0/g1的表现是相同的。那我们就要考虑了linux和darwin有哪些不同?这个可就多了,
没有办法去一一对比了。但是这些区别会很大程度反映到runtime上。


那我们就对runtime进行比较。那runtime中的什么会影响到程序的运行?可能有:(未列举全)


函数栈空间的扩展
goroutine的调度
io/syscall/cgo
gc

我们从上面的objdump的结果来看,生成的代码应该跟前3个因素都无关。那我们就尝试关闭gc,再进行一次比较:


import (
"runtime/debug"
"testing"
)
func init() {
debug.SetGCPercent(-1)
}
var a = make([]int, 1<<25)
func BenchmarkG0(b *testing.B) {
for i := 0; i < b.N; i++ {
g0(a)
}
}
func BenchmarkG1(b *testing.B) {
for i := 0; i < b.N; i++ {
g1(a)
}
}

然后


go test -c .
./len.test -test.bench=. -test.count=2

得到结果:


goos: darwin
goarch: amd64
BenchmarkG0-410011521770 ns/op
BenchmarkG0-410011310217 ns/op
BenchmarkG1-410011562763 ns/op
BenchmarkG1-410011590019 ns/op
PASS

可以看出关闭gc后,g0/g1表现相同,那是因为g1在运行过程中会动态分配内存么?上面objdump的结果来看,
明显不是。那只有可能是gc运行的时机问题,为什么gc偏巧要在g1运行时启动呢?这个就比较微妙了,runtime的
表现跟系统很多因素有关系,相同的代码在不同的操作系统上也有微妙的差异,或许有某种伪随机的因素在runtime中?
还未求证。


经过再次验证这种GC的影响与Benchmark函数的前后位置无关。



微信扫一扫

第七城市微信公众平台