Go 語言編譯器的 "//go:" 詳解

前言

C 語言的 #include

一上來不太好說明白 Go 語言裏 //go: 是什麼,咱們先來看下很是簡單,也是幾乎每一個寫代碼的人都知道的東西:C 語言的 #include
我猜,大部分人第一行代碼都是 #include 吧。完整的就是#include <stdio.h>。意思很簡單,引入一個 stdio.h。誰引入?答案是編譯器。那麼,# 字符的做用就是給 編譯器 一個 指示,讓編譯器知道接下來要作什麼。linux

編譯指示

在計算機編程中,編譯指示(pragma)是一種語言結構,它指示編譯器應該如何處理其輸入。指示不是編程語言語法的一部分,因編譯器而異。git

這裏 Wiki 詳細介紹了它,值得你看一下。

Go 語言的編譯指示

官方文檔 https://golang.org/cmd/compil...

形如 //go: 就是 Go 語言編譯指示的實現方式。相信看過 Go SDK 的同窗對此並不陌生,常常能在代碼函數聲明的上一行看到這樣的寫法。
有同窗會問了,// 這不是註釋嗎?確實,它是以註釋的形式存在的。github

編譯器源碼 這裏能夠看到所有的指示,可是要注意, //go: 是連續的, //go 之間並無空格。

經常使用指示詳解

//go:noinline

noinline 顧名思義,不要內聯。golang

Inline 內聯

Inline,是在編譯期間發生的,將函數調用調用處替換爲被調用函數主體的一種編譯器優化手段。Wiki: Inline 定義

使用 Inline 有一些優點,一樣也有一些問題。

優點:
  • 減小函數調用的開銷,提升執行速度。
  • 複製後的更大函數體爲其餘編譯優化帶來可能性,如 過程間優化
  • 消除分支,並改善空間局部性和指令順序性,一樣能夠提升性能。
問題:
  • 代碼複製帶來的空間增加。
  • 若是有大量重複代碼,反而會下降緩存命中率,尤爲對 CPU 緩存是致命的。

因此,在實際使用中,對因而否使用內聯,要謹慎考慮,並作好平衡,以使它發揮最大的做用。
簡單來講,對於短小並且工做較少的函數,使用內聯是有效益的。編程

內聯的例子

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 語言的編譯器指示,除非你確認你的程序的性能瓶頸在編譯器上,不然你都應該先去關心其餘更可能出現瓶頸的事情。

參考

相關文章
相關標籤/搜索