類似C++中的 #pragma pack(2),Golang中也有一些編譯指令汇陆。它們的實(shí)現(xiàn)方式是一些特殊的注釋毡代。
警告一下教寂!
編譯指令不是語言的一部分孝宗。它們可能是編譯器實(shí)現(xiàn)的因妇,編程規(guī)范中也沒有對它們的描述(更正一下婚被,現(xiàn)在有一部分指令的描述了https://golang.org/cmd/compile/
)址芯。
語法:
//go:directive
編譯指令的語法是一行特殊的注釋谷炸,關(guān)鍵字//和go之間沒有空格旬陡。
//go:noescape
func NewBook() (*Book) {
b := Book{ Mice: 12, Men: 9 }
return &b
}
這段代碼在C/C++中這樣做描孟,返回的是不可用的地址匿醒,顯然是要出問題的廉羔。在go中是可以的蜜另。因?yàn)樘右莘治鼍俟澹琤將會被分配在堆上此迅。
逃逸分析:
逃逸分析可以識別生命周期超出變量聲明函數(shù)的生命周期,并將變量從棧的分配上移動到堆中 Technically we say that b escapes to the heap.
func BuildLibrary() {
b := Book{Mice: 99: Men: 3}
AddToCollection(&b)
}
問題: b逃逸到了堆中鲁猩?
這取決于AddToCollection 對b做了什么
func AddToCollection(b *Book) {
b.Classification = "fiction"
}
逃逸分析發(fā)現(xiàn)AddToCollection并沒有將*book繼續(xù)傳遞廓握,所以此時(shí)b會被分配在棧上隙券。
但是娱仔,如果AddToCollection做了這樣的操作:
var AvailableForLoan [] *Book
func AddToCollection(b * Book){
AvailableForLoan = append(AvailableForLoan,b)
}
AddToCollection中將bappend到了一個(gè)生命周期更長的slice中盹憎,所以b必須被分配在堆上以保證的生命周期大于AddToCollection和BuildLibrary的脚乡。逃逸分析必須知道AddToCollection對b做了什么,調(diào)用了什么func 等等捡遍,以了解值是應(yīng)該分配在棧上還是堆上画株。這是逃逸分析的本質(zhì)谓传。
再看另一個(gè)例子:
os.File.Read
f, _ := os.Open("/tmp/foo")
buf := make([]byte, 4096)
n, _ := f.Read(buf)
我們打開一個(gè)文件续挟,創(chuàng)建一個(gè)buf诗祸,然后讀取數(shù)據(jù)到buf中直颅。此時(shí)buf是在棧上還是堆上功偿?
如上節(jié)所述械荷,這取決于Read內(nèi)部發(fā)生的事情征堪。os.Read通過幾層調(diào)用調(diào)到了syscall.Read佃蚜,然后又調(diào)到了syscall.Syscall來進(jìn)行操作系統(tǒng)調(diào)用谐算。而syscall.Syscall是在匯編中實(shí)現(xiàn)的洲脂,所以Go中的編譯器無法“看到”該函數(shù)的實(shí)現(xiàn)恐锦,因此無法判斷傳遞的值是否為escape一铅。由于編譯器不能知道是否需要escape所以潘飘,buf只能被判定為escape卜录。
回到//go:noescape編譯指令來
假設(shè)我們要用匯編寫一段glue code,類似bytes完域,md5凯傲,syscall 包冰单。
我們傳遞的值都會被分配在堆上诫欠。即使我們知道這樣做沒有必要荒叼。
package bytes
//go:noescape
// IndexByte returns the index of the first instance of c in s,
// or -1 if c is not present in s.
func IndexByte(s []byte, c byte) int // ../runtime/asm_$GOARCH.s
這就是//go:noescape的意義了,這個(gè)指令告訴編譯器 下面的func沒有任何參數(shù)escape嫁乘。編譯器將會跳過對func參數(shù)的檢查蜓斧。
//go:escape只能用于前置聲明(即 指令下的第一個(gè)func會受指令影響)
不過要格外關(guān)注的是挎春,這個(gè)命令會使代碼跳過編譯器的檢查直奋。如果弄錯(cuò)了就會破壞內(nèi)存帮碰,且沒有工具能發(fā)現(xiàn)這一點(diǎn)丰涉。
//go:norace
norace指令的用法和noescape一樣一死。Norace指令可以使編譯器跳過競爭檢測
鑒于競爭檢測器沒有已知的誤報(bào)投慈,應(yīng)該沒有理由將函數(shù)從其范圍中排除。
//go:nosplit
我們都知道goroutine的棧是可以動態(tài)自增的。Runtime會追蹤每個(gè)stack的使用情況扁誓。在運(yùn)行函數(shù)之前會進(jìn)行檢查以確保有足夠的椈雀遥空間來運(yùn)行該函數(shù)寿谴。如果不夠讶泰,代碼先進(jìn)入runtime擴(kuò)充stack。
但是有時(shí)這種開銷是不可接受的(偶爾也是不安全的)
//go:nosplit指令禁止stack拆分惠桃。但是這會導(dǎo)致一個(gè)問題辜王,如果你的堆棧耗盡呐馆,會發(fā)生什么//go:nosplit汹来?編譯器必須確保運(yùn)行函數(shù)是安全的,不能因?yàn)楸苊饬藯z查的開銷就讓函數(shù)使用比被允許空間更多的內(nèi)存摔桦。因?yàn)檫@樣做的話肯定會破壞其他goroutine的內(nèi)存空間邻耕。
為此兄世,編譯器維護(hù)一個(gè)名為redzone的緩沖區(qū)熙兔,一個(gè)768字節(jié)的住涉,分配在每個(gè)goroutines的堆椨呱框架底部媳握,保證可用蛾找。
編譯器會跟蹤每個(gè)函數(shù)的堆棧要求打毛。當(dāng)它遇到一個(gè)nosplit函數(shù)時(shí)幻枉,它會累積該函數(shù)對redzone的堆棧分配熬甫。通過這種方式,nosplit函數(shù)可以安全地對redzone緩沖區(qū)執(zhí)行覆旱,同時(shí)避免在不方便的時(shí)候堆棧增長。
(關(guān)于redzone的詳細(xì)描述 會單起一文)
package main
type T [256]byte // a large stack allocated type
//go:nosplit
func A(t T) {
B(t)
}
//go:nosplit
func B(t T) {
C(t)
}
//go:nosplit
func C(t T) {
D(t)
}
//go:nosplit
//go:noinline
func D(t T) {}
func main() {
var t T
A(t)
}
# command-line-arguments
main.C: nosplit stack overflow
744 assumed on entry to main.A (nosplit)
480 after main.A (nosplit) uses 264
472 on entry to main.B (nosplit)
208 after main.B (nosplit) uses 264
200 on entry to main.C (nosplit)
-64 after main.C (nosplit) uses 264
上面這段程序嘗試使用nosplit噪沙,但是不會編譯,因?yàn)榫幾g器檢測到redzone會耗盡
我們是否應(yīng)該在代碼中使用//go:nosplit局义?可以用萄唇,但是沒有必要。小函數(shù)從這種優(yōu)化中獲取的收益要比內(nèi)聯(lián)帶來的收益小四敞。上面的示例中用了//go:noinline禁止內(nèi)聯(lián)忿危。否則會檢測到D()什么也沒做,因此編譯器會優(yōu)化掉整個(gè)調(diào)用樹努释。
在所有指令中//go:nosplit是最安全的伐蒂。因?yàn)樗鼤诰幾g時(shí)被發(fā)現(xiàn)。并且不會影響程序正確性缕减,只會影響性能。
//go:noinlie
noinlie顧名思義裹芝,告訴編譯器不要inline嫂易。是否在我們的代碼中應(yīng)該這樣做呢怜械?我建議是不要用noinline指令的峡扩。
最后:
Go支持更多的編譯指令,但不在本文討論范圍
+build是Go tool而不是編譯器實(shí)現(xiàn)有额,為了過濾傳遞給編譯器的build或test文件。
編譯指令沒有出現(xiàn)在官方文檔中萤衰,編譯指令使用是有風(fēng)險(xiǎn)的。如果需要使用到這些編譯指令洒擦,請先翻閱相關(guān)指令在Golang源碼中的使用方法椿争。