Go 源碼裏的這些 --go: 指令,你知道嗎?
大家好,我是煎魚。
如果你平時有翻看源碼的習慣,你肯定會發現。咦,怎麼有的方法上面總是寫着 //go:
這類指令呢。他們到底是幹嘛用的?
今天和大家一同揭開他們的面紗,我將給你介紹一下他們的作用都是什麼。
go:linkname
//go:linkname localname importpath.name
該指令指示編譯器使用 importpath.name
作爲源代碼中聲明爲 localname
的變量或函數的目標文件符號名稱。但是由於這個僞指令,可以破壞類型系統和包模塊化,只有引用了 unsafe 包纔可以使用。
簡單來講,就是 importpath.name
是 localname
的符號別名,編譯器實際上會調用 localname
。
使用的前提是使用了 unsafe
包才能使用。
案例
time/time.go
...
func now() (sec int64, nsec int32, mono int64)
runtime/timestub.go
import _ "unsafe" // for go:linkname
//go:linkname time_now time.now
func time_now() (sec int64, nsec int32, mono int64) {
sec, nsec = walltime()
return sec, nsec, nanotime() - startNano
}
在這個案例中可以看到 time.now
,它並沒有具體的實現。如果你初看可能會懵逼。這時候建議你全局搜索一下源碼,你就會發現其實現在 runtime.time_now
中。
配合先前的用法解釋,可得知在 runtime 包中,我們聲明瞭 time_now
方法是 time.now
的符號別名。並且在文件頭引入了 unsafe
達成前提條件。
go:noescape
//go:noescape
該指令指定下一個有聲明但沒有主體(意味着實現有可能不是 Go)的函數,不允許編譯器對其做逃逸分析。
一般情況下,該指令用於內存分配優化。編譯器默認會進行逃逸分析,會通過規則判定一個變量是分配到堆上還是棧上。
但凡事有意外,一些函數雖然逃逸分析其是存放到堆上。但是對於我們來說,它是特別的。我們就可以使用 go:noescape
指令強制要求編譯器將其分配到函數棧上。
案例
// memmove copies n bytes from "from" to "to".
// in memmove_*.s
//go:noescape
func memmove(to, from unsafe.Pointer, n uintptr)
我們觀察一下這個案例,它滿足了該指令的常見特性。如下:
-
memmove_*.s:只有聲明,沒有主體。其主體是由底層彙編實現的
-
memmove:函數功能,在棧上處理性能會更好
go:nosplit
//go:nosplit
該指令指定文件中聲明的下一個函數不得包含堆棧溢出檢查。
簡單來講,就是這個函數跳過堆棧溢出的檢查。
案例
//go:nosplit
func key32(p *uintptr) *uint32 {
return (*uint32)(unsafe.Pointer(p))
}
go:nowritebarrierrec
//go:nowritebarrierrec
該指令表示編譯器遇到寫屏障時就會產生一個錯誤,並且允許遞歸。也就是這個函數調用的其他函數如果有寫屏障也會報錯。
簡單來講,就是針對寫屏障的處理,防止其死循環。
案例
//go:nowritebarrierrec
func gcFlushBgCredit(scanWork int64) {
...
}
go:yeswritebarrierrec
//go:yeswritebarrierrec
該指令與 go:nowritebarrierrec
相對,在標註 go:nowritebarrierrec
指令的函數上,遇到寫屏障會產生錯誤。
而當編譯器遇到 go:yeswritebarrierrec
指令時將會停止。
案例
//go:yeswritebarrierrec
func gchelper() {
...
}
go:noinline
該指令表示該函數禁止進行內聯。
案例
//go:noinline
func unexportedPanicForTesting(b []byte, i int) byte {
return b[i]
}
我們觀察一下這個案例,是直接通過索引取值,邏輯比較簡單。如果不加上 go:noinline
的話,就會出現編譯器對其進行內聯優化。
顯然,內聯有好有壞。該指令就是提供這一特殊處理。
go:norace
//go:norace
該指令表示禁止進行競態檢測。
常見的形式就是在啓動時執行 go run -race
,能夠檢測應用程序中是否存在雙向的數據競爭,非常有用。
案例
//go:norace
func forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr *ProcAttr, sys *SysProcAttr, pipe int) (pid int, err Errno) {
...
}
go:notinheap
//go:notinheap
該指令常用於類型聲明,它表示這個類型不允許從 GC 堆上進行申請內存。
在運行時中常用其來做較低層次的內部結構,避免調度器和內存分配中的寫屏障,能夠提高性能。
案例
// notInHeap is off-heap memory allocated by a lower-level allocator
// like sysAlloc or persistentAlloc.
//
// In general, it's better to use real types marked as go:notinheap,
// but this serves as a generic type for situations where that isn't
// possible (like in the allocators).
//
//go:notinheap
type notInHeap struct{}
總結
在本文我們簡單介紹了一些常見的指令集,讓大家有了整體的系統瞭解。這些指令平時在 Go 工程中我們是用不到的,常見的瓶頸可能更多的在自身應用上。
不過在瞭解了這些機制後,對你閱讀 Go 語言底層源碼和了解運行機制會很有幫助 :)
關注煎魚,吸取他的知識 👇
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7OLTcAGlvCEU3OUfpw5BRA