帶你了解五種加速Go的特性和如何實現(xiàn)它們
Anthony Starks 使用他出色的 Deck 演示工具重構(gòu)了我原來的基于 Google Slides 的幻燈片。你可以在他的博客上查看他重構(gòu)后的幻燈片,
mindchunk.blogspot.com.au/2014/06/remixing-with-deck。
我最近被邀請在 Gocon 發(fā)表演講,這是一個每半年在日本東京舉行的 Go 的精彩大會。Gocon 2014 是一個完全由社區(qū)驅(qū)動的為期一天的活動,由培訓和一整個下午的圍繞著生產(chǎn)環(huán)境中的 Go 這個主題的演講組成。(LCTT 譯注:本文發(fā)表于 2014 年)
以下是我的講義。原文的結(jié)構(gòu)能讓我緩慢而清晰的演講,因此我已經(jīng)編輯了它使其更可讀。 我要感謝 Bill Kennedy 和 Minux Ma,特別是 Josh Bleecher Snyder,感謝他們在我準備這次演講中的幫助。 大家下午好。 我叫 David. 我很高興今天能來到 Gocon。我想?yún)⒓舆@個會議已經(jīng)兩年了,我很感謝主辦方能提供給我向你們演講的機會。 Gocon 2014 我想以一個問題開始我的演講。 為什么選擇 Go? 當大家討論學習或在生產(chǎn)環(huán)境中使用 Go 的原因時,答案不一而足,但因為以下三個原因的最多。 Gocon 2014 這就是 TOP3 的原因。 ***,并發(fā)。 Go 的 并發(fā)原語 對于來自 Nodejs,Ruby 或 Python 等單線程腳本語言的程序員,或者來自 C++ 或 Java 等重量級線程模型的語言都很有吸引力。 易于部署。 我們今天從經(jīng)驗豐富的 Gophers 那里聽說過,他們非常欣賞部署 Go 應(yīng)用的簡單性。 Gocon 2014 然后是性能。 我相信人們選擇 Go 的一個重要原因是它 快。 Gocon 2014 (4) 在今天的演講中,我想討論五個有助于提高 Go 性能的特性。 我還將與大家分享 Go 如何實現(xiàn)這些特性的細節(jié)。 Gocon 2014 (5) 我要談的***個特性是 Go 對于值的高效處理和存儲。 Gocon 2014 (6) 這是 Go 中一個值的例子。編譯時, 讓我們將 Go 與其他一些語言進行比較 Gocon 2014 (7) 由于 Python 表示變量的方式的開銷,使用 Python 存儲相同的值會消耗六倍的內(nèi)存。 Python 使用額外的內(nèi)存來跟蹤類型信息,進行 引用計數(shù) 等。 讓我們看另一個例子: Gocon 2014 (8) 與 Go 類似,Java 消耗 4 個字節(jié)的內(nèi)存來存儲 但是,要在像 Gocon 2014 (9) 因此,Java 中的整數(shù)通常消耗 16 到 24 個字節(jié)的內(nèi)存。 為什么這很重要? 內(nèi)存便宜且充足,為什么這個開銷很重要? Gocon 2014 (10) 這是一張顯示 CPU 時鐘速度與內(nèi)存總線速度的圖表。 請注意 CPU 時鐘速度和內(nèi)存總線速度之間的差距如何繼續(xù)擴大。 兩者之間的差異實際上是 CPU 花費多少時間等待內(nèi)存。 Gocon 2014 (11) 自 1960 年代后期以來,CPU 設(shè)計師已經(jīng)意識到了這個問題。 他們的解決方案是一個緩存,一個更小、更快的內(nèi)存區(qū)域,介入 CPU 和主存之間。 Gocon 2014 (12) 這是一個 我們可以使用這種類型來構(gòu)造一個容納 1000 個 在數(shù)組內(nèi)部, 這很重要,因為現(xiàn)在所有 1000 個 Gocon 2014 (13) Go 允許您創(chuàng)建緊湊的數(shù)據(jù)結(jié)構(gòu),避免不必要的填充字節(jié)。 緊湊的數(shù)據(jù)結(jié)構(gòu)能更好地利用緩存。 更好的緩存利用率可帶來更好的性能。 Gocon 2014 (14) 函數(shù)調(diào)用不是無開銷的。 Gocon 2014 (15) 調(diào)用函數(shù)時會發(fā)生三件事。 創(chuàng)建一個新的 棧幀,并記錄調(diào)用者的詳細信息。 在函數(shù)調(diào)用期間可能被覆蓋的任何寄存器都將保存到棧中。 處理器計算函數(shù)的地址并執(zhí)行到該新地址的分支。 Gocon 2014 (16) 由于函數(shù)調(diào)用是非常常見的操作,因此 CPU 設(shè)計師一直在努力優(yōu)化此過程,但他們無法消除開銷。 函調(diào)固有開銷,或重于泰山,或輕于鴻毛,這取決于函數(shù)做了什么。 減少函數(shù)調(diào)用開銷的解決方案是 內(nèi)聯(lián)。 Gocon 2014 (17) Go 編譯器通過將函數(shù)體視為調(diào)用者的一部分來內(nèi)聯(lián)函數(shù)。 內(nèi)聯(lián)也有成本,它增加了二進制文件大小。 只有當調(diào)用開銷與函數(shù)所做工作關(guān)聯(lián)度的很大時內(nèi)聯(lián)才有意義,因此只有簡單的函數(shù)才能用于內(nèi)聯(lián)。 復雜的函數(shù)通常不受調(diào)用它們的開銷所支配,因此不會內(nèi)聯(lián)。 Gocon 2014 (18) 這個例子顯示函數(shù) 為了減少調(diào)用 Gocon 2014 (19) 內(nèi)聯(lián)后不再調(diào)用 內(nèi)聯(lián)并不是 Go 獨有的。幾乎每種編譯或及時編譯的語言都執(zhí)行此優(yōu)化。但是 Go 的內(nèi)聯(lián)是如何實現(xiàn)的? Go 實現(xiàn)非常簡單。編譯包時,會標記任何適合內(nèi)聯(lián)的小函數(shù),然后照常編譯。 然后函數(shù)的源代碼和編譯后版本都會被存儲。 Gocon 2014 (20) 此幻燈片顯示了 當編譯器編譯 就會替換原函數(shù)中的代碼,而不是插入對 擁有該函數(shù)的源代碼可以實現(xiàn)其他優(yōu)化。 Gocon 2014 (21) 在這個例子中,盡管函數(shù) 當 Gocon 2014 (22) 編譯器現(xiàn)在知道 這不僅節(jié)省了調(diào)用 Go 編譯器可以跨文件甚至跨包自動內(nèi)聯(lián)函數(shù)。還包括從標準庫調(diào)用的可內(nèi)聯(lián)函數(shù)的代碼。 Gocon 2014 (23) 強制垃圾回收 使 Go 成為一種更簡單,更安全的語言。 這并不意味著垃圾回收會使 Go 變慢,或者垃圾回收是程序速度的瓶頸。 這意味著在堆上分配的內(nèi)存是有代價的。每次 GC 運行時都會花費 CPU 時間,直到釋放內(nèi)存為止。 Gocon 2014 (24) 然而,有另一個地方分配內(nèi)存,那就是棧。 與 C 不同,它強制您選擇是否將值通過 Gocon 2014 (25) 逃逸分析決定了對一個值的任何引用是否會從被聲明的函數(shù)中逃逸。 如果沒有引用逃逸,則該值可以安全地存儲在棧中。 存儲在棧中的值不需要分配或釋放。 讓我們看一些例子 Gocon 2014 (26) 因為切片 沒有必要回收 Gocon 2014 (27) 第二個例子也有點尬。在 然后我們將 ***我們打印出那個 ‘Cursor` 的 X 和 Y 坐標。 即使 Gocon 2014 (28) 默認情況下,Go 的優(yōu)化始終處于啟用狀態(tài)。可以使用 因為逃逸分析是在編譯時執(zhí)行的,而不是運行時,所以無論垃圾回收的效率如何,棧分配總是比堆分配快。 我將在本演講的其余部分詳細討論棧。 Gocon 2014 (29) Go 有 goroutine。 這是 Go 并發(fā)的基石。 我想退一步,探索 goroutine 的歷史。 最初,計算機一次運行一個進程。在 60 年代,多進程或 分時 的想法變得流行起來。 在分時系統(tǒng)中,操作系統(tǒng)必須通過保護當前進程的現(xiàn)場,然后恢復另一個進程的現(xiàn)場,不斷地在這些進程之間切換 CPU 的注意力。 這稱為 進程切換。 Gocon 2014 (30) 進程切換有三個主要開銷。 首先,內(nèi)核需要保護該進程的所有 CPU 寄存器的現(xiàn)場,然后恢復另一個進程的現(xiàn)場。 內(nèi)核還需要將 CPU 的映射從虛擬內(nèi)存刷新到物理內(nèi)存,因為這些映射僅對當前進程有效。 ***是操作系統(tǒng) 上下文切換 的成本,以及 調(diào)度函數(shù) 選擇占用 CPU 的下一個進程的開銷。 Gocon 2014 (31) 現(xiàn)代處理器中有數(shù)量驚人的寄存器。我很難在一張幻燈片上排開它們,這可以讓你知道保護和恢復它們需要多少時間。 由于進程切換可以在進程執(zhí)行的任何時刻發(fā)生,因此操作系統(tǒng)需要存儲所有寄存器的內(nèi)容,因為它不知道當前正在使用哪些寄存器。 Gocon 2014 (32) 這導致了線程的出生,這些線程在概念上與進程相同,但共享相同的內(nèi)存空間。 由于線程共享地址空間,因此它們比進程更輕,因此創(chuàng)建速度更快,切換速度更快。 Gocon 2014 (33) Goroutine 升華了線程的思想。 Goroutine 是 協(xié)作式調(diào)度的,而不是依靠內(nèi)核來調(diào)度。 當對 Go 運行時調(diào)度器 進行顯式調(diào)用時,goroutine 之間的切換僅發(fā)生在明確定義的點上。 編譯器知道正在使用的寄存器并自動保存它們。 Gocon 2014 (34) 雖然 goroutine 是協(xié)作式調(diào)度的,但運行時會為你處理。 Goroutine 可能會給禪讓給其他協(xié)程時刻是: Gocon 2014 (35) 這個例子說明了上一張幻燈片中描述的一些調(diào)度點。 箭頭所示的線程從左側(cè)的 繼續(xù)執(zhí)行直到從通道 調(diào)度器將線程切換回右側(cè)以進行另一個通道操作,該操作在左側(cè)運行期間已解鎖,但在通道發(fā)送時再次阻塞。 ***,當 Gocon 2014 (36) 這張幻燈片顯示了低級語言描述的 只要你的代碼調(diào)用操作系統(tǒng),就會通過此函數(shù)。 對 這允許運行時啟動一個新線程,該線程將在當前線程被阻塞時為其他 goroutine 提供服務(wù)。 這導致每 Go 進程的操作系統(tǒng)線程相對較少,Go 運行時負責將可運行的 Goroutine 分配給空閑的操作系統(tǒng)線程。 Gocon 2014 (37) 在上一節(jié)中,我討論了 goroutine 如何減少管理許多(有時是數(shù)十萬個并發(fā)執(zhí)行線程)的開銷。 Goroutine故事還有另一面,那就是棧管理,它引導我進入我的***一個話題。 Gocon 2014 (38) 這是一個進程的內(nèi)存布局圖。我們感興趣的關(guān)鍵是堆和棧的位置。 傳統(tǒng)上,在進程的地址空間內(nèi),堆位于內(nèi)存的底部,位于程序(代碼)的上方并向上增長。 棧位于虛擬地址空間的頂部,并向下增長。 Gocon 2014 (39) 因為堆和棧相互覆蓋的結(jié)果會是災(zāi)難性的,操作系統(tǒng)通常會安排在棧和堆之間放置一個不可寫內(nèi)存區(qū)域,以確保如果它們發(fā)生碰撞,程序?qū)⒅兄埂?/p>
這稱為保護頁,有效地限制了進程的棧大小,通常大約為幾兆字節(jié)。 Gocon 2014 (40) 我們已經(jīng)討論過線程共享相同的地址空間,因此對于每個線程,它必須有自己的棧。 由于很難預(yù)測特定線程的棧需求,因此為每個線程的棧和保護頁面保留了大量內(nèi)存。 希望是這些區(qū)域永遠不被使用,而且防護頁永遠不會被擊中。 缺點是隨著程序中線程數(shù)的增加,可用地址空間的數(shù)量會減少。 Gocon 2014 (41) 我們已經(jīng)看到 Go 運行時將大量的 goroutine 調(diào)度到少量線程上,但那些 goroutines 的棧需求呢? Go 編譯器不使用保護頁,而是在每個函數(shù)調(diào)用時插入一個檢查,以檢查是否有足夠的棧來運行該函數(shù)。如果沒有,運行時可以分配更多的??臻g。 由于這種檢查,goroutines 初始??梢宰龅酶?,這反過來允許 Go 程序員將 goroutines 視為廉價資源。 Gocon 2014 (42) 這是一張顯示了 Go 1.2 如何管理棧的幻燈片。 當 Gocon 2014 (43) 這種管理棧的方法通常很好用,但對于某些類型的代碼,通常是遞歸代碼,它可能導致程序的內(nèi)部循環(huán)跨越這些棧邊界之一。 例如,在程序的內(nèi)部循環(huán)中,函數(shù) 每次都會導致棧拆分。 這被稱為 熱分裂 問題。 Gocon 2014 (44) 為了解決熱分裂問題,Go 1.3 采用了一種新的棧管理方法。 如果 goroutine 的棧太小,則不會添加和刪除其他棧段,而是分配新的更大的棧。 舊棧的內(nèi)容被復制到新棧,然后 goroutine 使用新的更大的棧繼續(xù)運行。 在***次調(diào)用 這解決了熱分裂問題。 Gocon 2014 (45) 值,內(nèi)聯(lián),逃逸分析,Goroutines 和分段/復制棧。 這些是我今天選擇談?wù)摰奈鍌€特性,但它們絕不是使 Go 成為快速的語言的唯一因素,就像人們引用他們學習 Go 的理由的三個原因一樣。 這五個特性一樣強大,它們不是孤立存在的。 例如,運行時將 goroutine 復用到線程上的方式在沒有可擴展棧的情況下幾乎沒有效率。 內(nèi)聯(lián)通過將較小的函數(shù)組合成較大的函數(shù)來降低棧大小檢查的成本。 逃逸分析通過自動將從實例從堆移動到棧來減少垃圾回收器的壓力。 逃逸分析還提供了更好的 緩存局部性。 如果沒有可增長的棧,逃逸分析可能會對棧施加太大的壓力。 Gocon 2014 (46) gocon
正好消耗四個字節(jié)的內(nèi)存。int
型。List
或 Map
這樣的集合中使用此值,編譯器必須將其轉(zhuǎn)換為 Integer
對象。Location
類型,它保存物體在三維空間中的位置。它是用 Go 編寫的,因此每個 Location
只消耗 24 個字節(jié)的存儲空間。Location
的數(shù)組類型,它只消耗 24000 字節(jié)的內(nèi)存。Location
結(jié)構(gòu)體是順序存儲的,而不是隨機存儲的 1000 個 Location
結(jié)構(gòu)體的指針。Location
結(jié)構(gòu)體都按順序放在緩存中,緊密排列在一起。Double
調(diào)用 util.Max
。util.Max
的開銷,編譯器可以將 util.Max
內(nèi)聯(lián)到 Double
中,就象這樣util.Max
,但是 Double
的行為沒有改變。util.a
的內(nèi)容。源代碼已經(jīng)過一些轉(zhuǎn)換,以便編譯器更容易快速處理。Double
時,它看到 util.Max
可內(nèi)聯(lián)的,并且 util.Max
的源代碼是可用的。util.Max
的編譯版本的調(diào)用。Test
總是返回 false
,但 Expensive
在不執(zhí)行它的情況下無法知道結(jié)果。Test
被內(nèi)聯(lián)時,我們得到這樣的東西。Expensive
的代碼無法訪問。Test
的成本,還節(jié)省了編譯或運行任何現(xiàn)在無法訪問的 Expensive
代碼。malloc
將其存儲在堆上,還是通過在函數(shù)范圍內(nèi)聲明將其儲存在棧上;Go 實現(xiàn)了一個名為 逃逸分析 的優(yōu)化。Sum
返回 1 到 100 的整數(shù)的和。這是一種相當不尋常的做法,但它說明了逃逸分析的工作原理。numbers
僅在 Sum
內(nèi)引用,所以編譯器將安排到棧上來存儲的 100 個整數(shù),而不是安排到堆上。numbers
,它會在 Sum
返回時自動釋放。CenterCursor
中,我們創(chuàng)建一個新的 Cursor
對象并在 c
中存儲指向它的指針。c
傳遞給 Center()
函數(shù),它將 Cursor
移動到屏幕的中心。c
被 new
函數(shù)分配了空間,它也不會存儲在堆上,因為沒有引用 c
的變量逃逸 CenterCursor
函數(shù)。-gcflags = -m
開關(guān)查看編譯器的逃逸分析和內(nèi)聯(lián)決策。
ReadFile
函數(shù)開始。遇到 os.Open
,它在等待文件操作完成時阻塞線程,因此調(diào)度器將線程切換到右側(cè)的 goroutine。c
中讀,并且此時 os.Open
調(diào)用已完成,因此調(diào)度器將線程切換回左側(cè)并繼續(xù)執(zhí)行 file.Read
函數(shù),然后又被文件 IO 阻塞。Read
操作完成并且數(shù)據(jù)可用時,線程切換回左側(cè)。runtime.Syscall
函數(shù),它是 os
包中所有函數(shù)的基礎(chǔ)。entersyscall
的調(diào)用通知運行時該線程即將阻塞。G
調(diào)用 H
時,沒有足夠的空間讓 H
運行,所以運行時從堆中分配一個新的棧幀,然后在新的棧段上運行 H
。當 H
返回時,棧區(qū)域返回到堆,然后返回到 G
。G
可以在循環(huán)中多次調(diào)用 H
,H
之后,棧將足夠大,對可用??臻g的檢查將始終成功。