一上來不太好說明白 Go 語言裏 //go:
是什麼,咱們先來看下很是簡單,也是幾乎每一個寫代碼的人都知道的東西:C 語言的 #include
。
我猜,大部分人第一行代碼都是 #include
吧。完整的就是#include <stdio.h>
。意思很簡單,引入一個 stdio.h
。誰引入?答案是編譯器。那麼,#
字符的做用就是給 編譯器 一個 指示,讓編譯器知道接下來要作什麼。linux
在計算機編程中,編譯指示(pragma)
是一種語言結構,它指示編譯器應該如何處理其輸入。指示
不是編程語言語法的一部分,因編譯器而異。git
這裏 Wiki 詳細介紹了它,值得你看一下。
官方文檔 https://golang.org/cmd/compil...
形如 //go:
就是 Go 語言編譯指示的實現方式。相信看過 Go SDK 的同窗對此並不陌生,常常能在代碼函數聲明的上一行看到這樣的寫法。
有同窗會問了,//
這不是註釋嗎?確實,它是以註釋的形式存在的。github
編譯器源碼 這裏能夠看到所有的指示,可是要注意,//go:
是連續的,//
和go
之間並無空格。
//go:noinline
noinline
顧名思義,不要內聯。golang
Inline
,是在編譯期間發生的,將函數調用調用處替換爲被調用函數主體的一種編譯器優化手段。Wiki:
Inline 定義
Inline
有一些優點,一樣也有一些問題。因此,在實際使用中,對因而否使用內聯,要謹慎考慮,並作好平衡,以使它發揮最大的做用。
簡單來講,對於短小並且工做較少的函數,使用內聯是有效益的。編程
func appendStr(word string) string { return "new " + word }
執行 GOOS=linux GOARCH=386 go tool compile -S main.go > main.S
我截取有區別的部分展出它編譯後的樣子:segmentfault
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
函數,而是直接把這個函數體的功能內聯了。緩存
那麼話說回來,若是你不想被內聯,怎麼辦呢?此時就該使用 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)
此時編譯器就不會作內聯,而是直接調用 appendStr
函數。併發
//go:nosplit
nosplit
的做用是:跳過棧溢出檢測。
正是由於一個 Goroutine 的起始棧大小是有限制的,且比較小的,才能夠作到支持併發不少 Goroutine,並高效調度。
stack.go 源碼中能夠看到,_StackMin
是 2048 字節,也就是 2k,它不是一成不變的,當不夠用時,它會動態地增加。
那麼,必然有一個檢測的機制,來保證能夠及時地知道棧不夠用了,而後再去增加。
回到話題,nosplit
就是將這個跳過這個機制。
顯然地,不執行棧溢出檢查,能夠提升性能,但同時也有可能發生 stack overflow
而致使編譯失敗。
//go:noescape
noescape
的做用是:禁止逃逸,並且它必須指示一個只有聲明沒有主體的函數。
Go 相比 C、C++ 是內存更爲安全的語言,主要一個點就體如今它能夠自動地將超出自身生命週期的變量,從函數棧轉移到堆中,逃逸就是指這種行爲。
請參考我以前的文章, 逃逸分析。
最顯而易見的好處是,GC 壓力變小了。
由於它已經告訴編譯器,下面的函數不管如何都不會逃逸,那麼當函數返回時,其中的資源也會一併都被銷燬。
不過,這麼作表明會繞過編譯器的逃逸檢查,一旦進入運行時,就有可能致使嚴重的錯誤及後果。
//go:norace
norace
的做用是:跳過競態檢測
咱們知道,在多線程程序中,不免會出現數據競爭,正常狀況下,當編譯器檢測到有數據競爭,就會給出提示。如:
var sum int func main() { go add() go add() } func add() { sum++ }
執行 go run -race main.go
利用 -race
來使編譯器報告數據競爭問題。你會看到:
================== 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 執行的 add()
在競爭。
使用 norace
除了減小編譯時間,我想不到有其餘的優勢了。但缺點卻很明顯,那就是數據競爭會致使程序的不肯定性。
我認爲絕大多數狀況下,無需在編程時使用 //go:
Go 語言的編譯器指示,除非你確認你的程序的性能瓶頸在編譯器上,不然你都應該先去關心其餘更可能出現瓶頸的事情。