這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
翻譯原文連結 轉帖/轉載請註明出處
英文原文連結 發表於2014/06/07
Goroutine的棧管理
在上一篇文章裡,我們已經討論了goroutine減少了對上百個並發啟動並執行線程的管理開銷。這裡我們要在討論下goroutine的另外一個方面,它的棧管理。
下面是一個進程的記憶體布局圖。這個圖裡我們關心的是堆和棧的位置。
通常在進程的定址空間裡,堆是在記憶體的底部,從程式的可執行指令儲存空間(text)開始向上衍生。棧則位於虛擬位址空間的頂部,並向下衍生。
如果堆和棧衍生後相互覆蓋的話,那結果是災難性的。作業系統通常會在堆和棧之間設定一塊不可寫的記憶體地區。如果堆和棧衍生到一起的話,程式就會退出。這塊記憶體地區叫做保護頁(guard page)。它限制了進程的棧的大小。這個限制通常在幾M的量級。
我們前面講到過線程是共用定址空間的。但是每個線程,它又必須要有它自己的棧。
因為很難預測一個線程需要多大的棧空間,很多的記憶體空間都被預留給了線程的棧和保護頁以期望預留的棧空間足夠大,這樣保護頁不會被觸及到。這樣做的缺點就是隨著程式裡線程數的增加,可用的定址空間也相應地減少了。
我們已經討論過Go的運行環境會把大量的goroutine在少數的線程上調度。那麼這些goroutine的棧是怎麼管理的呢?
Go語言沒有使用保護頁機制。Go的編譯器會在每個函數調用的時候插入一段代碼來檢查是否有足夠的棧空間來運行被調用的函數。如果空間不足,Go的運行環境就會分配更多的棧空間。因為有了這個檢查機制,一個goroutine的初始棧可以很小。這樣Go程式員就可以把goroutine作為相對廉價的資源來使用。
顯示了Go 1.2裡是如何管理棧的。
當G調用H的時候,沒有足夠的棧空間來讓H運行,這時候Go運行環境就會從堆裡分配一個新的棧記憶體塊去讓H運行。在H返回到G之前,新分配的記憶體塊被釋放回堆。這種管理棧的方法一般都工作得很好。但對有些代碼,特別是遞迴調用,它會造成程式不停地分配和釋放新的記憶體空間。舉個例子,在一個程式裡,函數G會在一個迴圈裡調用很多次H函數。每次調用都會分配一塊新的記憶體空間。這就是熱分裂問題(hot split problem)。
為瞭解決這個問題,Go 1.3採用了新的棧管理方法。
如果goroutine的棧太小了,它會去分配一塊新的更大的棧,而不是分配和釋法額外的記憶體空間。老的棧裡的內容被複製到新的棧裡,goroutine會在新的棧上繼續執行。在第一次調用H函數之後,將會有足夠大的棧空間,這樣以後的棧空間大小檢查都不會有問題了。這就解決了熱分裂問題。
變數,內聯,逃逸分析,Goroutines,和棧的分塊/複製管理就是今天我要討論的5個特性。當然Go語言高效不僅僅是因為這5個特性。這就像大家有千奇百怪的理由來學習Go語言一樣,這些理由也絕不止3個。
這些特性個個都很有效,他們之間還相互依賴。比如,如果沒有了可衍生的棧,運行環境將多個goroutine複用到線程上面就不會很有效。內聯在把多個小函數合并成大函數的時候也避免了棧大小的檢查開銷。逃逸分析用棧代替堆來儲存局部變數,這樣也減少來記憶體回收機制的壓力。逃逸分析還提升了緩衝的效能(cache locality)。沒有可衍生的棧,逃逸分析又會對棧造成很大的壓力。