前言
C 語言的 #include
一上來不太好說明白 Go 語言里 //go: 是什么,我們先來看下非常簡單,也是幾乎每個寫代碼的人都知道的東西:C 語言的 #include。
我猜,大部分人第一行代碼都是 #include 吧。完整的就是#include <stdio.h>。意思很簡單,引入一個 stdio.h。誰引入?答案是編譯器。那么,# 字符的作用就是給 編譯器 一個 指示,讓編譯器知道接下來要做什么。
編譯指示
在計算機(jī)編程中,編譯指示(pragma)是一種語言結(jié)構(gòu),它指示編譯器應(yīng)該如何處理其輸入。指示不是編程語言語法的一部分,因編譯器而異。
這里Wiki詳細(xì)介紹了它,值得你看一下。
Go 語言的編譯指示
形如 //go: 就是 Go 語言編譯指示的實現(xiàn)方式。相信看過 Go SDK 的同學(xué)對此并不陌生,經(jīng)常能在代碼函數(shù)聲明的上一行看到這樣的寫法。
有同學(xué)會問了,// 這不是注釋嗎?確實,它是以注釋的形式存在的。
編譯器源碼 這里可以看到全部的指示,但是要注意,
//go:是連續(xù)的,//和go之間并沒有空格。
常用指示詳解
//go:noinline
noinline 顧名思義,不要內(nèi)聯(lián)。
Inline 內(nèi)聯(lián)
Inline,是在編譯期間發(fā)生的,將函數(shù)調(diào)用調(diào)用處替換為被調(diào)用函數(shù)主體的一種編譯器優(yōu)化手段。Wiki:Inline 定義
使用 Inline 有一些優(yōu)勢,同樣也有一些問題。
優(yōu)勢:
- 減少函數(shù)調(diào)用的開銷,提高執(zhí)行速度。
- 復(fù)制后的更大函數(shù)體為其他編譯優(yōu)化帶來可能性,如 過程間優(yōu)化
- 消除分支,并改善空間局部性和指令順序性,同樣可以提高性能。
劣勢:
- 代碼復(fù)制帶來的空間增長。
- 如果有大量重復(fù)代碼,反而會降低緩存命中率,尤其對 CPU 緩存是致命的。
所以,在實際使用中,對于是否使用內(nèi)聯(lián),要謹(jǐn)慎考慮,并做好平衡,以使它發(fā)揮最大的作用。
簡單來說,對于短小而且工作較少的函數(shù),使用內(nèi)聯(lián)是有效益的。
內(nèi)聯(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)
可以看到,它并沒有調(diào)用 appendStr 函數(shù),而是直接把這個函數(shù)體的功能內(nèi)聯(lián)了。
那么話說回來,如果你不想被內(nèi)聯(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)
此時編譯器就不會做內(nèi)聯(lián),而是直接調(diào)用 appendStr 函數(shù)。
//go:nosplit
nosplit 的作用是:跳過棧溢出檢測。
棧溢出是什么?
正是因為一個 Goroutine 的起始棧大小是有限制的,且比較小的,才可以做到支持并發(fā)很多 Goroutine,并高效調(diào)度。
stack.go 源碼中可以看到,_StackMin 是 2048 字節(jié),也就是 2k,它不是一成不變的,當(dāng)不夠用時,它會動態(tài)地增長。
那么,必然有一個檢測的機(jī)制,來保證可以及時地知道棧不夠用了,然后再去增長。
回到話題,nosplit 就是將這個跳過這個機(jī)制。
優(yōu)劣
顯然地,不執(zhí)行棧溢出檢查,可以提高性能,但同時也有可能發(fā)生 stack overflow 而導(dǎo)致編譯失敗。
//go:noescape
noescape 的作用是:禁止逃逸,而且它必須指示一個只有聲明沒有主體的函數(shù)。
逃逸是什么?
Go 相比 C、C++ 是內(nèi)存更為安全的語言,主要一個點就體現(xiàn)在它可以自動地將超出自身生命周期的變量,從函數(shù)棧轉(zhuǎn)移到堆中,逃逸就是指這種行為。
請參考我之前的文章,逃逸分析。
優(yōu)劣
最顯而易見的好處是,GC 壓力變小了。
因為它已經(jīng)告訴編譯器,下面的函數(shù)無論如何都不會逃逸,那么當(dāng)函數(shù)返回時,其中的資源也會一并都被銷毀。
不過,這么做代表會繞過編譯器的逃逸檢查,一旦進(jìn)入運(yùn)行時,就有可能導(dǎo)致嚴(yán)重的錯誤及后果。
//go:norace
norace 的作用是:跳過競態(tài)檢測
我們知道,在多線程程序中,難免會出現(xiàn)數(shù)據(jù)競爭,正常情況下,當(dāng)編譯器檢測到有數(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ù)競爭會導(dǎo)致程序的不確定性。
總結(jié)
我認(rèn)為絕大多數(shù)情況下,無需在編程時使用 //go: Go 語言的編譯器指示,除非你確認(rèn)你的程序的性能瓶頸在編譯器上,否則你都應(yīng)該先去關(guān)心其他更可能出現(xiàn)瓶頸的事情。
本文來自:Segmentfault
感謝作者:sxssxs
查看原文:Go 語言編譯器的 "http://go:" 詳解