詳解Go內(nèi)聯(lián)優(yōu)化

為了保證程序的執(zhí)行高效與安全肿仑,現(xiàn)代編譯器并不會(huì)將程序員的代碼直接翻譯成相應(yīng)地機(jī)器碼匾乓,它需要做一系列的檢查與優(yōu)化迅皇。Go編譯器默認(rèn)做了很多相關(guān)工作贷笛,例如未使用的引用包檢查用狱、未使用的聲明變量檢查禽拔、有效的括號檢查埠帕、逃逸分析拔恰、內(nèi)聯(lián)優(yōu)化褪尝、刪除無用代碼等闹获。本文重點(diǎn)討論內(nèi)聯(lián)優(yōu)化相關(guān)內(nèi)容。

內(nèi)聯(lián)

在《詳解逃逸分析》一文中河哑,我們分析了棧分配內(nèi)存會(huì)比堆分配高效地多避诽,那么,我們就會(huì)希望對象能盡可能被分配在棧上璃谨。在Go中沙庐,一個(gè)goroutine會(huì)有一個(gè)單獨(dú)的棧鲤妥,棧又會(huì)包含多個(gè)棧幀,棧幀是函數(shù)調(diào)用時(shí)在棧上為函數(shù)所分配的區(qū)域拱雏。但其實(shí)棉安,函數(shù)調(diào)用是存在一些固定開銷的,例如維護(hù)幀指針寄存器BP铸抑、棧溢出檢測等贡耽。因此,對于一些代碼行比較少的函數(shù)鹊汛,編譯器傾向于將它們在編譯期展開從而消除函數(shù)調(diào)用蒲赂,這種行為就是內(nèi)聯(lián)。

性能對比

首先柒昏,看一下函數(shù)內(nèi)聯(lián)與非內(nèi)聯(lián)的性能差異凳宙。

//go:noinline
func maxNoinline(a, b int) int {
    if a < b {
        return b
    }
    return a
}

func maxInline(a, b int) int {
    if a < b {
        return b
    }
    return a
}

func BenchmarkNoInline(b *testing.B) {
    x, y := 1, 2
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        maxNoinline(x, y)
    }
}

func BenchmarkInline(b *testing.B) {
    x, y := 1, 2
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        maxInline(x, y)
    }
}

在程序代碼中,想要禁止編譯器內(nèi)聯(lián)優(yōu)化很簡單职祷,在函數(shù)定義前一行添加//go:noinline即可氏涩。以下是性能對比結(jié)果

BenchmarkNoInline-8     824031799                1.47 ns/op
BenchmarkInline-8       1000000000               0.255 ns/op

因?yàn)楹瘮?shù)體內(nèi)部的執(zhí)行邏輯非常簡單,此時(shí)內(nèi)聯(lián)與否的性能差異主要體現(xiàn)在函數(shù)調(diào)用的固定開銷上有梆。顯而易見是尖,該差異是非常大的。

內(nèi)聯(lián)場景

此時(shí)泥耀,愛思考的讀者可能就會(huì)產(chǎn)生疑問:既然內(nèi)聯(lián)優(yōu)化效果這么顯著饺汹,是不是所有的函數(shù)調(diào)用都可以內(nèi)聯(lián)呢?答案是不可以痰催。因?yàn)閮?nèi)聯(lián)兜辞,其實(shí)就是將一個(gè)函數(shù)調(diào)用原地展開,替換成這個(gè)函數(shù)的實(shí)現(xiàn)夸溶。當(dāng)該函數(shù)被多次調(diào)用逸吵,就會(huì)被多次展開,這會(huì)增加編譯后二進(jìn)制文件的大小缝裁。而非內(nèi)聯(lián)函數(shù)扫皱,只需要保存一份函數(shù)體的代碼,然后進(jìn)行調(diào)用捷绑。所以韩脑,在空間上,一般來說使用內(nèi)聯(lián)函數(shù)會(huì)導(dǎo)致生成的可執(zhí)行文件變大(但需要考慮內(nèi)聯(lián)的代碼量粹污、調(diào)用次數(shù)段多、維護(hù)內(nèi)聯(lián)關(guān)系的開銷)。

問題來了壮吩,編譯器內(nèi)聯(lián)優(yōu)化的選擇策略是什么进苍?

package main

func add(a, b int) int {
    return a + b
}

func iter(num int) int {
    res := 1
    for i := 1; i <= num; i++ {
        res = add(res, i)
    }
    return res
}

func main() {
    n := 100
    _ = iter(n)
}

假設(shè)源碼文件為main.go蕾总,可通過執(zhí)行go build -gcflags="-m -m" main.go命令查看編譯器的優(yōu)化策略。

$ go build -gcflags="-m -m" main.go
# command-line-arguments
./main.go:3:6: can inline add with cost 4 as: func(int, int) int { return a + b }
./main.go:7:6: cannot inline iter: unhandled op FOR
./main.go:10:12: inlining call to add func(int, int) int { return a + b }
./main.go:15:6: can inline main with cost 67 as: func() { n := 100; _ = iter(n) }

通過以上信息琅捏,可知編譯器判斷add函數(shù)與main函數(shù)都可以被內(nèi)聯(lián)優(yōu)化生百,并將add函數(shù)內(nèi)聯(lián)。同時(shí)可以注意到的是柄延,iter函數(shù)由于存在循環(huán)語句并不能被內(nèi)聯(lián):cannot inline iter: unhandled op FOR蚀浆。實(shí)際上,除了for循環(huán)搜吧,還有一些情況不會(huì)被內(nèi)聯(lián)市俊,例如閉包,select滤奈,for摆昧,defergo關(guān)鍵字所開啟的新goroutine等蜒程,詳細(xì)可見src/cmd/compile/internal/gc/inl.go相關(guān)內(nèi)容绅你。

    case OCLOSURE,
        OCALLPART,
        ORANGE,
        OFOR,
        OFORUNTIL,
        OSELECT,
        OTYPESW,
        OGO,
        ODEFER,
        ODCLTYPE, // can't print yet
        OBREAK,
        ORETJMP:
        v.reason = "unhandled op " + n.Op.String()
        return true

在上文提到過,內(nèi)聯(lián)只針對小代碼量的函數(shù)而言昭躺,那么到底是小于多少才算是小代碼量呢忌锯?

此時(shí),我將上面的add函數(shù)领炫,更改為如下內(nèi)容

func add(a, b int) int {
    a = a + 1
    return a + b
}

執(zhí)行go build -gcflags="-m -m" main.go命令偶垮,得到信息

./main.go:3:6: can inline add with cost 9 as: func(int, int) int { a = a + 1; return a + b }

對比之前的信息

./main.go:3:6: can inline add with cost 4 as: func(int, int) int { return a + b }

可以發(fā)現(xiàn),存在cost 4cost 9的區(qū)別帝洪。這里的數(shù)值代表的是抽象語法樹AST的節(jié)點(diǎn)似舵,a = a + 1包含的是5個(gè)節(jié)點(diǎn)。Go函數(shù)中超過80個(gè)節(jié)點(diǎn)的代碼量就不再內(nèi)聯(lián)葱峡。例如砚哗,如果在add中寫入16個(gè)a = a + 1,則不再內(nèi)聯(lián)族沃。

./main.go:3:6: cannot inline add: function too complex: cost 84 exceeds budget 80
內(nèi)聯(lián)表

內(nèi)聯(lián)會(huì)將函數(shù)調(diào)用的過程抹掉频祝,這會(huì)引入一個(gè)新的問題:代碼的堆棧信息還能否保證泌参。舉個(gè)例子脆淹,如果程序發(fā)生panic,內(nèi)聯(lián)之后的程序沽一,還能否準(zhǔn)確的打印出堆棧信息盖溺?看以下例子。

package main

func sub(a, b int) {
    a = a - b
    panic("i am a panic information")
}

func max(a, b int) int {
    if a < b {
        sub(a, b)
    }
    return a
}

func main() {
    x, y := 1, 2
    _ = max(x, y)
}

在該代碼樣例中铣缠,max函數(shù)將被內(nèi)聯(lián)烘嘱。執(zhí)行程序昆禽,輸出結(jié)果如下

panic: i am a panic information

goroutine 1 [running]:
main.sub(...)
        /Users/slp/go/src/workspace/example/main.go:5
main.max(...)
        /Users/slp/go/src/workspace/example/main.go:10
main.main()
        /Users/slp/go/src/workspace/example/main.go:17 +0x3a

我們可以發(fā)現(xiàn),panic依然輸出了正確的程序堆棧信息蝇庭,包括源文件位置和行號信息醉鳖。那,Go是如何做到的呢哮内?這是由于Go內(nèi)部會(huì)為每個(gè)存在內(nèi)聯(lián)優(yōu)化的goroutine維持一個(gè)內(nèi)聯(lián)樹(inlining tree)盗棵,該樹可通過 go build -gcflags="-d pctab=pctoinline" main.go 命令查看

funcpctab "".sub [valfunc=pctoinline]
...
wrote 3 bytes to 0xc000082668
 00 42 00
funcpctab "".max [valfunc=pctoinline]
...
wrote 7 bytes to 0xc000082f68
 00 3c 02 1d 01 09 00
-- inlining tree for "".max:
0 | -1 | "".sub (/Users/slp/go/src/workspace/example/main.go:10:6) pc=59
--
funcpctab "".main [valfunc=pctoinline]
...
wrote 11 bytes to 0xc0004807e8
 00 1d 02 01 01 07 04 16 03 0c 00
-- inlining tree for "".main:
0 | -1 | "".max (/Users/slp/go/src/workspace/example/main.go:17:9) pc=30
1 | 0 | "".sub (/Users/slp/go/src/workspace/example/main.go:10:6) pc=29
--
內(nèi)聯(lián)控制

Go程序編譯時(shí),默認(rèn)將進(jìn)行內(nèi)聯(lián)優(yōu)化北发。我們可通過-gcflags="-l"選項(xiàng)全局禁用內(nèi)聯(lián)纹因,與一個(gè)-l禁用內(nèi)聯(lián)相反,如果傳遞兩個(gè)或兩個(gè)以上的-l則會(huì)打開內(nèi)聯(lián)琳拨,并啟用更激進(jìn)的內(nèi)聯(lián)策略瞭恰。如果不想全局范圍內(nèi)禁止優(yōu)化,則可以在函數(shù)定義時(shí)添加 //go:noinline 編譯指令來阻止編譯器內(nèi)聯(lián)函數(shù)狱庇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惊畏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子密任,更是在濱河造成了極大的恐慌陕截,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件批什,死亡現(xiàn)場離奇詭異农曲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驻债,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門乳规,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人合呐,你說我怎么就攤上這事暮的。” “怎么了淌实?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵冻辩,是天一觀的道長。 經(jīng)常有香客問我拆祈,道長恨闪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任放坏,我火速辦了婚禮咙咽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘淤年。我一直安慰自己钧敞,他們只是感情好蜡豹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溉苛,像睡著了一般镜廉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上愚战,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天桨吊,我揣著相機(jī)與錄音,去河邊找鬼凤巨。 笑死视乐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的敢茁。 我是一名探鬼主播佑淀,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼彰檬!你這毒婦竟也來了伸刃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逢倍,失蹤者是張志新(化名)和其女友劉穎捧颅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體较雕,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碉哑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亮蒋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扣典。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慎玖,靈堂內(nèi)的尸體忽然破棺而出贮尖,到底是詐尸還是另有隱情,我是刑警寧澤趁怔,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布湿硝,位于F島的核電站,受9級特大地震影響润努,放射性物質(zhì)發(fā)生泄漏关斜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一任连、第九天 我趴在偏房一處隱蔽的房頂上張望蚤吹。 院中可真熱鬧例诀,春花似錦随抠、人聲如沸裁着。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽二驰。三九已至,卻和暖如春秉沼,著一層夾襖步出監(jiān)牢的瞬間桶雀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工唬复, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矗积,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓敞咧,卻偏偏與公主長得像棘捣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子休建,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 C 語言的 #include 一上來不太好說明白 Go 語言里 //go: 是什么乍恐,我們先來看下非常簡單,也...
    Chole121閱讀 2,470評論 0 9
  • 本文為國外Gopher的常見錯(cuò)誤總結(jié)测砂,以下為出處: 原文:http://devs.cloudimmunity.co...
    GoFuncChan閱讀 1,033評論 0 5
  • 內(nèi)聯(lián)茵烈,就是將一個(gè)函數(shù)調(diào)用原地展開,替換成這個(gè)函數(shù)的實(shí)現(xiàn)砌些。盡管這樣做會(huì)增加編譯后二進(jìn)制文件的大小呜投,但是它可以提高程序...
    樸素的心態(tài)閱讀 1,936評論 1 10
  • 目錄 統(tǒng)一規(guī)范篇 命名篇 開發(fā)篇 優(yōu)化篇 統(tǒng)一規(guī)范篇 本篇主要描述了公司內(nèi)部同事都必須遵守的一些開發(fā)規(guī)矩,如統(tǒng)一開...
    零一間閱讀 1,931評論 0 2
  • 零 前置知識 操作系統(tǒng)的每個(gè)進(jìn)程都認(rèn)為自己可以訪問計(jì)算機(jī)的所有物理內(nèi)存存璃,但由于計(jì)算機(jī)必定運(yùn)行著多個(gè)程序宙彪,每個(gè)進(jìn)程都...
    voidFan閱讀 1,178評論 0 1