這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近在同事提出了一個疑問:在對一個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: darwingoarch: amd64BenchmarkG0-4 100 11784627 ns/opBenchmarkG0-4 100 11841061 ns/opBenchmarkG1-4 100 18623122 ns/opBenchmarkG1-4 100 17790754 ns/opPASS
果然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 488b442410 MOVQ 0x10(SP), AX main.go:4 0x10ef155 31c9 XORL CX, CX main.go:6 0x10ef157 eb03 JMP 0x10ef15c main.go:6 0x10ef159 48ffc1 INCQ CX main.go:6 0x10ef15c 4839c1 CMPQ AX, CX main.go:6 0x10ef15f 7cf8 JL 0x10ef159 main.go:8 0x10ef161 48c744242001000000 MOVQ $0x1, 0x20(SP) main.go:8 0x10ef16a c3 RET :-1 0x10ef16b cc INT $0x3 :-1 0x10ef16c cc INT $0x3 :-1 0x10ef16d cc INT $0x3 :-1 0x10ef16e cc INT $0x3 :-1 0x10ef16f cc INT $0x3TEXT _/test/go/len.g1(SB) /test/go/len/main.go main.go:12 0x10ef170 488b442410 MOVQ 0x10(SP), AX main.go:12 0x10ef175 31c9 XORL CX, CX main.go:13 0x10ef177 eb03 JMP 0x10ef17c main.go:13 0x10ef179 48ffc1 INCQ CX main.go:13 0x10ef17c 4839c1 CMPQ AX, CX main.go:13 0x10ef17f 7cf8 JL 0x10ef179 main.go:15 0x10ef181 48c744242001000000 MOVQ $0x1, 0x20(SP) main.go:15 0x10ef18a c3 RET :-1 0x10ef18b cc INT $0x3 :-1 0x10ef18c cc INT $0x3 :-1 0x10ef18d cc INT $0x3 :-1 0x10ef18e cc INT $0x3 :-1 0x10ef18f cc INT $0x3
我們可以看到編譯器產生的中間代碼完全是相同的,那為什麼在運行起來會有不同的結果呢?那我們就要考慮了,除了代碼以外,還有什麼會影響代碼執行?那就是:
那我們就分別對這兩個因素進行驗證。
首先是運行環境,我們換到linux
上再進行一次驗證:
GOOS=linux GOARCH=amd64 go test -c .## copy to linux./test.len -test.bench=. -test.count=2
得到輸出結果:
goos: linuxgoarch: amd64BenchmarkG0-32 100 10824437 ns/opBenchmarkG0-32 100 10743979 ns/opBenchmarkG1-32 100 10740347 ns/opBenchmarkG1-32 100 10898047 ns/opPASS
在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: darwingoarch: amd64BenchmarkG0-4 100 11521770 ns/opBenchmarkG0-4 100 11310217 ns/opBenchmarkG1-4 100 11562763 ns/opBenchmarkG1-4 100 11590019 ns/opPASS
可以看出關閉gc
後,g0
/g1
表現相同,那是因為g1
在運行過程中會動態分配記憶體嗎?上面objdump的結果來看,明顯不是。那隻有可能是gc啟動並執行時機問題,為什麼gc偏巧要在g1
運行時啟動呢?這個就比較微妙了,runtime
的表現跟系統很多因素有關係,相同的代碼在不同的作業系統上也有微妙的差異,或許有某種偽隨機的因素在runtime
中?還未求證。
經過再次驗證這種GC的影響與Benchmark函數的前後位置無關。