Go 語言編譯器的 "//go:" 詳解
【轉發(fā)】https://segmentfault.com/a/1190000016743220
前言
C 語言的 #include
一上來不太好說明白 Go 語言里 //go: 是什么,我們先來看下非常簡單,也是幾乎每個寫代碼的人都知道的東西:C 語言的 #include。
我猜,大部分人第一行代碼都是 #include 吧。完整的就是#include <stdio.h>。意思很簡單,引入一個 stdio.h。誰引入?答案是編譯器。那么,# 字符的作用就是給 編譯器 一個 指示,讓編譯器知道接下來要做什么。
編譯指示
在計算機編程中,編譯指示(pragma)是一種語言結構,它指示編譯器應該如何處理其輸入。指示不是編程語言語法的一部分,因編譯器而異。
這里 Wiki 詳細介紹了它,值得你看一下。
Go 語言的編譯指示
官方文檔 https://golang.org/cmd/compil...
形如 //go: 就是 Go 語言編譯指示的實現(xiàn)方式。相信看過 Go SDK 的同學對此并不陌生,經(jīng)常能在代碼函數(shù)聲明的上一行看到這樣的寫法。
有同學會問了,// 這不是注釋嗎?確實,它是以注釋的形式存在的。
編譯器源碼 這里可以看到全部的指示,但是要注意,//go:是連續(xù)的,//和go之間并沒有空格。
常用指示詳解
//go:noinline
noinline 顧名思義,不要內聯(lián)。
Inline 內聯(lián)
Inline,是在編譯期間發(fā)生的,將函數(shù)調用調用處替換為被調用函數(shù)主體的一種編譯器優(yōu)化手段。Wiki:Inline 定義
使用 Inline 有一些優(yōu)勢,同樣也有一些問題。
優(yōu)勢:
- 減少函數(shù)調用的開銷,提高執(zhí)行速度。
- 復制后的更大函數(shù)體為其他編譯優(yōu)化帶來可能性,如 過程間優(yōu)化
- 消除分支,并改善空間局部性和指令順序性,同樣可以提高性能。
問題:
- 代碼復制帶來的空間增長。
- 如果有大量重復代碼,反而會降低緩存命中率,尤其對 CPU 緩存是致命的。
所以,在實際使用中,對于是否使用內聯(lián),要謹慎考慮,并做好平衡,以使它發(fā)揮最大的作用。
簡單來說,對于短小而且工作較少的函數(shù),使用內聯(lián)是有效益的。
內聯(lián)的例子
func appendStr(word string) string {
return "new " + word
}
執(zhí)行 GOOS=linux GOARCH=386 go tool compile -S main.go > main.S
我截取有區(qū)別的部分展出它編譯后的樣子:
0x0015 00021 (main.go:4) LEAL ""..autotmp_3+28(SP), AX
0x0019 00025 (main.go:4) PCDATA $2, $0
0x0019 00025 (main.go:4) MOVL AX, (SP)
0x001c 00028 (main.go:4) PCDATA $2, $1
0x001c 00028 (main.go:4) LEAL go.string."new "(SB), AX
0x0022 00034 (main.go:4) PCDATA $2, $0
0x0022 00034 (main.go:4) MOVL AX, 4(SP)
0x0026 00038 (main.go:4) MOVL $4, 8(SP)
0x002e 00046 (main.go:4) PCDATA $2, $1
0x002e 00046 (main.go:4) LEAL go.string."hello"(SB), AX
0x0034 00052 (main.go:4) PCDATA $2, $0
0x0034 00052 (main.go:4) MOVL AX, 12(SP)
0x0038 00056 (main.go:4) MOVL $5, 16(SP)
0x0040 00064 (main.go:4) CALL runtime.concatstring2(SB)
可以看到,它并沒有調用 appendStr 函數(shù),而是直接把這個函數(shù)體的功能內聯(lián)了。
那么話說回來,如果你不想被內聯(lián),怎么辦呢?此時就該使用 go//:noinline 了,像下面這樣寫:
//go:noinline
func appendStr(word string) string {
return "new " + word
}
編譯后是:
0x0015 00021 (main.go:4) LEAL go.string."hello"(SB), AX
0x001b 00027 (main.go:4) PCDATA $2, $0
0x001b 00027 (main.go:4) MOVL AX, (SP)
0x001e 00030 (main.go:4) MOVL $5, 4(SP)
0x0026 00038 (main.go:4) CALL "".appendStr(SB)
此時編譯器就不會做內聯(lián),而是直接調用 appendStr 函數(shù)。
//go:nosplit
nosplit 的作用是:跳過棧溢出檢測。
棧溢出是什么?
正是因為一個 Goroutine 的起始棧大小是有限制的,且比較小的,才可以做到支持并發(fā)很多 Goroutine,并高效調度。
stack.go 源碼中可以看到,_StackMin 是 2048 字節(jié),也就是 2k,它不是一成不變的,當不夠用時,它會動態(tài)地增長。
那么,必然有一個檢測的機制,來保證可以及時地知道棧不夠用了,然后再去增長。
回到話題,nosplit 就是將這個跳過這個機制。
優(yōu)劣
顯然地,不執(zhí)行棧溢出檢查,可以提高性能,但同時也有可能發(fā)生 stack overflow 而導致編譯失敗。
//go:noescape
noescape 的作用是:禁止逃逸,而且它必須指示一個只有聲明沒有主體的函數(shù)。
逃逸是什么?
Go 相比 C、C++ 是內存更為安全的語言,主要一個點就體現(xiàn)在它可以自動地將超出自身生命周期的變量,從函數(shù)棧轉移到堆中,逃逸就是指這種行為。
請參考我之前的文章,逃逸分析。
優(yōu)劣
最顯而易見的好處是,GC 壓力變小了。
因為它已經(jīng)告訴編譯器,下面的函數(shù)無論如何都不會逃逸,那么當函數(shù)返回時,其中的資源也會一并都被銷毀。
不過,這么做代表會繞過編譯器的逃逸檢查,一旦進入運行時,就有可能導致嚴重的錯誤及后果。
//go:norace
norace 的作用是:跳過競態(tài)檢測
我們知道,在多線程程序中,難免會出現(xiàn)數(shù)據(jù)競爭,正常情況下,當編譯器檢測到有數(shù)據(jù)競爭,就會給出提示。如:
var sum int
func main() {
go add()
go add()
}
func add() {
sum++
}
執(zhí)行 go run -race main.go 利用 -race 來使編譯器報告數(shù)據(jù)競爭問題。你會看到:
==================
WARNING: DATA RACE
Read at 0x00000112f470 by goroutine 6:
main.add()
/Users/sxs/Documents/go/src/test/main.go:15 +0x3a
Previous write at 0x00000112f470 by goroutine 5:
main.add()
/Users/sxs/Documents/go/src/test/main.go:15 +0x56
Goroutine 6 (running) created at:
main.main()
/Users/sxs/Documents/go/src/test/main.go:11 +0x5a
Goroutine 5 (finished) created at:
main.main()
/Users/sxs/Documents/go/src/test/main.go:10 +0x42
==================
Found 1 data race(s)
說明兩個 goroutine 執(zhí)行的 add() 在競爭。
優(yōu)劣
使用 norace 除了減少編譯時間,我想不到有其他的優(yōu)點了。但缺點卻很明顯,那就是數(shù)據(jù)競爭會導致程序的不確定性。
總結
我認為絕大多數(shù)情況下,無需在編程時使用 //go: Go 語言的編譯器指示,除非你確認你的程序的性能瓶頸在編譯器上,否則你都應該先去關心其他更可能出現(xiàn)瓶頸的事情。

浙公網(wǎng)安備 33010602011771號