閱讀go源碼,你需要了解這幾個(gè)編譯器指示
長(zhǎng)安城里的一切都在無可避免的走向庸俗。

談到編譯器指示,我們?cè)谄綍r(shí)工作中幾乎不會(huì)使用,除非你覺得你的代碼瓶頸出現(xiàn)在編譯期,不過了解掌握編譯器指示對(duì)于我們閱讀golang源碼還是挺有幫助的。
什么是編譯器指示?
編譯器接受注釋形式的指示。比如我們常見的//go:xxx的形式出現(xiàn)在方法前面上方。為了將其與非指示注釋區(qū)分開,編譯器指示要求在注釋開頭和指示名稱之間不需要空格。但是由于它們是注釋,故而不了解指示約定或特定指示的工具可以像其他注釋一樣跳過指示。其大體分為兩大類:
以 // line或/ * line開頭的行指示
行指示有如下幾種形式:
//line :line
//line :line:col
//line filename:line
//line filename:line:col
/*line :line*/
/*line :line:col*/
/*line filename:line*/
/*line filename:line:col*/
為了被識(shí)別為行指示,注釋必須以// line或/ * line開頭,后跟一個(gè)空格,并且必須至少包含一個(gè)冒號(hào)。行指示是歷史上的特例,主要出現(xiàn)在機(jī)器生成的代碼中,以便編譯器和調(diào)試器將原始輸入中的位置報(bào)告給生成器。故而這個(gè)不是我們今天的重點(diǎn)。
以 //go:name形式的指示
這種形式的編譯器指示都必須放在自己的行中,注釋前只能有空格和制表符。每個(gè)指示都緊隨其后的Go代碼,該代碼通常必須是一個(gè)聲明。我們今天主要來認(rèn)識(shí)幾個(gè)常見的這種形式的編譯器指示
編譯器指示分類
//go:noescape
//go:noescape指示后面必須跟沒有主體的函數(shù)聲明(意味著該函數(shù)具有非Go編寫的實(shí)現(xiàn)),它指定函數(shù)不允許作為參數(shù)傳遞的任何指針逃逸到堆中或函數(shù)返回值中。編譯器在對(duì)調(diào)用該函數(shù)的Go代碼進(jìn)行逃逸分析時(shí),可以使用此信息。
啥是逃逸?
逃逸分析屬于編譯器優(yōu)化的一種方式,Go內(nèi)存也是分為堆和棧,相比C、C++在棧還是堆上分配內(nèi)存是程序員手動(dòng)控制的,而在Go中,如果一個(gè)值超過了函數(shù)調(diào)用的生命周期,編譯器會(huì)自動(dòng)將其從函數(shù)棧轉(zhuǎn)移到堆中。這種行為被稱為逃逸。
阻止了變量逃逸到堆上,最顯而易見的好處是GC壓力小了。
但缺點(diǎn)是:這么做意味著繞過了編譯器的逃逸分析,無論如何都不會(huì)出現(xiàn)逃逸,函數(shù)返回則其相關(guān)的資源也一并銷毀,使用不當(dāng)運(yùn)行時(shí)很可能導(dǎo)致嚴(yán)重后果。
//go:linkname
//go:linkname是初看go源碼常見的一個(gè)編譯器指示,因?yàn)橛袝r(shí)候你跟著跟著就發(fā)現(xiàn)函數(shù)只有聲明沒有函數(shù)體,也沒有匯編實(shí)現(xiàn)。
//go:linkname localname importpath.name
該編譯器指示作用是使用importpath.name作為源碼中聲明為localname的變量或函數(shù)的目標(biāo)文件符號(hào)名稱。但這樣就破壞了類型系統(tǒng)和模塊化,因此只有引用了unsafe包才可以使用。這么解釋可能有點(diǎn)兒繞,簡(jiǎn)單來說就是 我們importpath.name來調(diào)用時(shí)實(shí)際執(zhí)行的是localname 但前提引用了unsafe
舉個(gè)栗子:sync.Mutex進(jìn)行Lock操作時(shí)如果goroutine搶占鎖失敗會(huì)調(diào)用runtime_SemacquireMutex(&m.sema, queueLifo, 1)來阻塞等待。
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
實(shí)際函數(shù)實(shí)現(xiàn)在runtime/sema.go
import "unsafe"
//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}
//go:nowritebarrier
//go:nowritebarrier告訴編譯器如果跟著的函數(shù)包含寫屏障則觸發(fā)一個(gè)錯(cuò)誤,但并不會(huì)阻止寫屏障的生成。
//go:nowritebarrierrec和//go:yeswritebarrierrec
這對(duì)編譯器指示蠻有意思的。主要出現(xiàn)在調(diào)度器代碼中。//go:nowritebarrierrec告訴編譯器當(dāng)前函數(shù)及其調(diào)用的函數(shù)(允許遞歸)直到發(fā)現(xiàn)//go:yeswritebarrierrec為止,若期間遇到寫屏障則觸發(fā)一個(gè)錯(cuò)誤。
這對(duì)編譯器指示都是在調(diào)度器中使用。寫屏障需要一個(gè)活躍的P,但是調(diào)度器中的相關(guān)代碼可能不需要一個(gè)活躍的P的情況下運(yùn)行。此時(shí),//go:nowritebarrierrec用在不需要P的函數(shù)上,而//go:yeswritebarrierrec用在重新獲取P的函數(shù)上。
例如:runtime.main運(yùn)行時(shí)調(diào)用sysmon運(yùn)行不需要P
// Always runs without a P, so write barriers are not allowed.
//go:nowritebarrierrec
func sysmon() {
... ...
}
//go:systemstack
//go:systemstack表示函數(shù)必須在系統(tǒng)棧上運(yùn)行。
如 :分配npages頁的手動(dòng)管理的一個(gè)span
//go:systemstack
func (h *mheap) allocManual(npages uintptr, typ spanAllocType) *mspan {
if !typ.manual() {
throw("manual span allocation called with non-manually-managed type")
}
return h.allocSpan(npages, typ, 0)
}
//go:noinheap
//go:noinheap適用于類型聲明,表示一個(gè)類型必須不能分配到GC堆上。好處是runtime在底層結(jié)構(gòu)中使用它來避免調(diào)度器和內(nèi)存分配中的寫屏障以避免非法檢查或提高性能。
//go:noinline
inline是編譯期將函數(shù)調(diào)用處替換為被調(diào)用函數(shù)主體的一種編譯優(yōu)化手段,//go:noinline意思就是不要內(nèi)聯(lián)。
優(yōu)勢(shì) 減少函數(shù)調(diào)用開銷 提高執(zhí)行速度 替換后更大函數(shù)體為其他編譯優(yōu)化提供可能 消除分支改善空間局部性和指令順序性 缺點(diǎn) 代碼復(fù)制帶來的空間增長(zhǎng) 大量重復(fù)代碼會(huì)降低緩存命中率
內(nèi)聯(lián)是把雙刃劍,在我們實(shí)際使用過程,你需要謹(jǐn)慎考慮做好平衡。//go:noinline編譯器指示為我們做平衡提供了一種手段。
//go:nosplit
//go:nosplit作用是跳過棧溢出檢測(cè)。
什么是棧溢出?
一個(gè)goroutine的初始棧大小是有限制的,并且比較小,所以才可以支持并發(fā)很多的goroutine,并且高效調(diào)度。實(shí)際上每個(gè)新的goroutine會(huì)被runtime分配初始化2KB大小的棧空間。但它的大小并不是一直保持不變的,隨著一個(gè)goroutine進(jìn)行工作的過程中,可能會(huì)超出最初分配的棧空間的限制,也就是可能棧溢出。
那這個(gè)時(shí)候怎么辦呢?為防止這種情況發(fā)生,runtime確保goroutine在不夠用的時(shí)候,會(huì)創(chuàng)建一個(gè)相當(dāng)于原來兩倍大小的新棧,并將原來?xiàng)5纳舷挛目截惖叫聴I希@個(gè)過程稱為棧分裂(stack-split),這樣使得goroutine棧能夠動(dòng)態(tài)調(diào)整大小。那么必然需要有一個(gè)檢測(cè)的機(jī)制,來保證可以及時(shí)地知道棧不夠用了,然后再去增長(zhǎng)。
實(shí)際上編譯器是通過每一個(gè)函數(shù)的開頭和結(jié)束位置插入指令防止goroutine爆棧
而我們確定一定不會(huì)爆棧的函數(shù),可以用//go:nosplit來提示編譯器跳過這個(gè)機(jī)制,不要再這些函數(shù)的開頭和結(jié)束部分插入這些檢查指令。
這樣做不執(zhí)行棧溢出檢查,雖然可以提高性能,但同時(shí)使用不當(dāng)也有可能發(fā)生stack overflow而導(dǎo)致編譯失敗。
栗子:
channel發(fā)送的代碼
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
//go:norace
//go:norace表示禁止進(jìn)行競(jìng)態(tài)檢測(cè)。它指定競(jìng)態(tài)檢測(cè)器必須忽略函數(shù)的內(nèi)存訪問。除了節(jié)約了點(diǎn)編譯時(shí)間沒發(fā)現(xiàn)啥其他好處。
如果閱讀過程中發(fā)現(xiàn)本文存疑或錯(cuò)誤的地方,可以關(guān)注公眾號(hào)留言。如果覺得還可以 幫忙點(diǎn)個(gè)在看??
