原文連接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 協議進行受權許可。
本節介紹Go編譯器執行的三個重要優化。git
Go 編譯器在2007年左右開始做爲 Plan9 編譯器工具鏈的一個分支。當時的編譯器與 Aho 和 Ullman 的 Dragon Book 很是類似。程序員
2015年,當時的 Go 1.5 編譯器 從 C 機械地翻譯成 Go。github
一年後,Go 1.7 引入了一個基於 SSA 技術的 新編譯器後端 ,取代了以前的 Plan 9風格的代碼。這個新的後端爲泛型和體系結構特定的優化提供了許多可能。golang
咱們要討論的第一個優化是逃逸分析。後端
爲了說明逃逸分析,首先讓咱們來回憶一下在 Go spec 中沒有提到堆和棧,它只提到 Go 語言是有垃圾回收的,但也沒有說明如何是如何實現的。安全
一個遵循 Go spec 的 Go 實現能夠將每一個分配操做都在堆上執行。這會給垃圾回收器帶來很大壓力,但這樣作是絕對錯誤的 -- 多年來,gccgo對逃逸分析的支持很是有限,因此才致使這樣作被認爲是有效的。函數
然而,goroutine 的棧是做爲存儲局部變量的廉價場所而存在;沒有必要在棧上執行垃圾回收。所以,在棧上分配內存也是更加安全和有效的。工具
在一些語言中,如C
和C++
,在棧仍是堆上分配內存由程序員手動決定——堆分配使用malloc
和free
,而棧分配經過alloca
。錯誤地使用這種機制會是致使內存錯誤的常見緣由。優化
在 Go 中,若是一個值超過了函數調用的生命週期,編譯器會自動將之移動到堆中。咱們管這種現象叫:該值逃逸到了堆。ui
type Foo struct { a, b, c, d int } func NewFoo() *Foo { return &Foo{a: 3, b: 1, c: 4, d: 7} }
在這個例子中,NewFoo
函數中分配的 Foo
將被移動到堆中,所以在 NewFoo
返回後 Foo
仍然有效。
這是從早期的 Go 就開始有的。與其說它是一種優化,不如說它是一種自動正確性特性。沒法在 Go 中返回棧上分配的變量的地址。
同時編譯器也能夠作相反的事情;它能夠找到堆上要分配的東西,並將它們移動到棧上。
讓咱們來看下面的例子:
// Sum 函數返回 0-100 的整數之和 func Sum() int { const count = 100 numbers := make([]int, count) for i := range numbers { numbers[i] = i + 1 } var sum int for _, i := range numbers { sum += i } return sum }
Sum
將 0-100 的 ints
型數字相加並返回結果。
由於 numbers
切片僅在 Sum
函數內部使用,編譯器將在棧上存儲這100個整數而不是堆。也沒有必要對 numbers
進行垃圾回收,由於它會在 Sum
返回時自動釋放。
證實它!
要打印編譯器關於逃逸分析的決策,請使用-m
標誌。
% go build -gcflags=-m examples/esc/sum.go # command-line-arguments examples/esc/sum.go:8:17: Sum make([]int, count) does not escape examples/esc/sum.go:22:13: answer escapes to heap examples/esc/sum.go:22:13: main ... argument does not escape
第8行顯示編譯器已正確推斷 make([]int, 100)
的結果不會逃逸到堆。
第22行顯示answer
逃逸到堆的緣由是fmt.Println
是一個可變函數。 可變參數函數的參數被裝入一個切片,在本例中爲[]interface{}
,因此會將answer
賦值爲接口值,由於它是經過調用fmt.Println
引用的。 從 Go 1.6(多是)開始,垃圾收集器須要經過接口傳遞的全部值都是指針,編譯器看到的是這樣的:
var answer = Sum() fmt.Println([]interface{&answer}...)
咱們可使用標識 -gcflags="-m -m"
來肯定這一點。會返回:
examples/esc/sum.go:22:13: answer escapes to heap examples/esc/sum.go:22:13: from ... argument (arg to ...) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: from *(... argument) (indirection) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: from ... argument (passed to call[argument content escapes]) at examples/esc/sum.go:22:13 examples/esc/sum.go:22:13: main ... argument does not escape
總之,不要擔憂第22行,這對咱們的討論並不重要。
這個例子是咱們模擬的。 它不是真正的代碼,只是一個例子。
package main import "fmt" type Point struct{ X, Y int } const Width = 640 const Height = 480 func Center(p *Point) { p.X = Width / 2 p.Y = Height / 2 } func NewPoint() { p := new(Point) Center(p) fmt.Println(p.X, p.Y) }
NewPoint
建立了一個 *Point
指針值 p
。 咱們將p
傳遞給Center
函數,該函數將點移動到屏幕中心的位置。最後咱們打印出 p.X
和 p.Y
的值。
% go build -gcflags=-m examples/esc/center.go # command-line-arguments examples/esc/center.go:10:6: can inline Center examples/esc/center.go:17:8: inlining call to Center examples/esc/center.go:10:13: Center p does not escape examples/esc/center.go:18:15: p.X escapes to heap examples/esc/center.go:18:20: p.Y escapes to heap examples/esc/center.go:16:10: NewPoint new(Point) does not escape examples/esc/center.go:18:13: NewPoint ... argument does not escape # command-line-arguments
儘管p
是使用new
分配的,但它不會存儲在堆上,由於Center
被內聯了,因此沒有p
的引用會逃逸到Center
函數。
在 Go 中,函數調用有固定的開銷;棧和搶佔檢查。
硬件分支預測器改善了其中的一些功能,但就功能大小和時鐘週期而言,這仍然是一個成本。
內聯是避免這些成本的經典優化方法。
內聯只對葉子函數有效,葉子函數是不調用其餘函數的。這樣作的理由是:
還有一個緣由就是嚴重的內聯會使得堆棧信息更加難以跟蹤。
func Max(a, b int) int { if a > b { return a } return b } func F() { const a, b = 100, 20 if Max(a, b) == b { panic(b) } }
咱們再次使用 -gcflags = -m
標識來查看編譯器優化決策。
% go build -gcflags=-m examples/max/max.go # command-line-arguments examples/max/max.go:3:6: can inline Max examples/max/max.go:12:8: inlining call to Max
編譯器打印了兩行信息:
Max
的聲明告訴咱們它能夠內聯Max
的主體已經內聯到第12行調用者中。編譯 max.go
而後咱們看看優化版本的 F()
變成什麼樣了。
% go build -gcflags=-S examples/max/max.go 2>&1 | grep -A5 '"".F STEXT' "".F STEXT nosplit size=1 args=0x0 locals=0x0 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) TEXT "".F(SB), NOSPLIT, $0-0 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (<unknown line number>) RET 0x0000 c3
一旦Max
被內聯到這裏,這就是F的主體 - 這個函數什麼都沒幹。我知道屏幕上有不少沒用的文字,可是相信個人話,惟一發生的就是RET
。實際上F
變成了:
func F() { return }
注意 : 利用 -S
的輸出並非進入二進制文件的最終機器碼。連接器在最後的連接階段進行一些處理。像FUNCDATA
和PCDATA
這樣的行是垃圾收集器的元數據,它們在連接時移動到其餘位置。 若是你正在讀取-S
的輸出,請忽略FUNCDATA
和PCDATA
行;它們不是最終二進制的一部分。
使用-gcflags=-l
標識調整內聯級別。有些使人困惑的是,傳遞一個-l
將禁用內聯,兩個或兩個以上將在更激進的設置中啓用內聯。
-gcflags=-l
,禁用內聯。-gcflags='-l -l'
內聯級別2,更積極,可能更快,可能會製做更大的二進制文件。-gcflags='-l -l -l'
內聯級別3,再次更加激進,二進制文件確定更大,也許更快,但也許會有 bug。-gcflags=-l=4
(4個 -l
) 在 Go 1.11 中將支持實驗性的 中間棧內聯優化。爲何a
和b
是常數很重要?
爲了理解發生了什麼,讓咱們看一下編譯器在把Max
內聯到F
中的時候看到了什麼。咱們不能輕易地從編譯器中得到這個,可是直接手動完成它。
Before:
func Max(a, b int) int { if a > b { return a } return b } func F() { const a, b = 100, 20 if Max(a, b) == b { panic(b) } }
After:
func F() { const a, b = 100, 20 var result int if a > b { result = a } else { result = b } if result == b { panic(b) } }
由於a
和b
是常量,因此編譯器能夠在編譯時證實分支永遠不會是假的;100
老是大於20
。所以它能夠進一步優化 F
爲
func F() { const a, b = 100, 20 var result int if true { result = a } else { result = b } if result == b { panic(b) } }
既然分支的結果已經知道了,那麼結果的內容也就知道了。這叫作分支消除。
func F() { const a, b = 100, 20 const result = a if result == b { panic(b) } }
如今分支被消除了,咱們知道結果老是等於a
,而且由於a
是常數,咱們知道結果是常數。 編譯器將此證實應用於第二個分支
func F() { const a, b = 100, 20 const result = a if false { panic(b) } }
而且再次使用分支消除,F
的最終形式減小成這樣。
func F() { const a, b = 100, 20 const result = a }
最後就變成
func F() { }
分支消除是一種被稱爲死碼消除的優化。實際上,使用靜態證實來代表一段代碼永遠不可達,一般稱爲死代碼,所以它不須要在最終的二進制文件中編譯、優化或發出。
咱們發現死碼消除與內聯一塊兒工做,以減小循環和分支產生的代碼數量,這些循環和分支被證實是不可到達的。
你能夠利用這一點來實現昂貴的調試,並將其隱藏起來
const debug = false
結合構建標記,這可能很是有用。
編譯器標識提供以下:
go build -gcflags=$FLAGS
研究如下編譯器功能的操做:
-S
打印正在編譯的包的彙編代碼-l
控制內聯行爲; -l
禁止內聯, -l -l
增長-l
(更多-l
會增長編譯器對代碼內聯的強度)。試驗編譯時間,程序大小和運行時間的差別。-m
控制優化決策的打印,如內聯,逃逸分析。-m
打印關於編譯器的想法的更多細節。-l -N
禁用全部優化。注意 : If you find that subsequent runs of go build ...
produce no output, delete the ./max
binary in your working directory.