Go 部落格
踏上 Go:Go 垃圾回收器的歷程
這是 2018 年 6 月 18 日我在國際記憶體管理研討會 (ISMM) 上所發表主題演講的逐字記錄稿。在過去 25 年來,ISMM 一直是發表記憶體管理和垃圾回收論文的主要場地,而受邀發表主題演講對我而言是莫大的榮幸。
摘要
Go 語言的功能、目標和使用案例迫使我們重新思考整個垃圾回收堆疊,並將我們帶到一個令人驚訝的地方。這段旅程令人振奮。本演講描述了我們的旅程。這段旅程的動力來自於開放原始碼和 Google 的產品需求。其中也包括旁敲側擊的死巷,而數字帶領我們找到回家之路。本演講將提供關於我們的旅程的緣由與方式、我們在 2018 年的位置,以及 Go 為旅程下一階段準備的見解。
履歷
理查·L·赫德森 (瑞克) 最為人所知的是他在記憶體管理方面的成就,包括發明火車演算法、藍寶石演算法和密西西比三角洲演算法,以及可在 Modula-3、Java、C# 和 Go 等靜態型別語言中進行垃圾回收的 GC 堆疊映射。瑞克目前是 Google Go 團隊的成員,他在 Go 的垃圾回收和執行時間問題上進行專案。
聯絡資訊:rlh@golang.org
評論:另請參閱 golang-dev 上的討論。
謄稿

我是 Rick Hudson。
這場演講主題是 Go 執行階段,尤其是垃圾收集器。我準備了大約 45 到 50 分鐘的素材,之後我們將有時間進行討論,我也會在現場等著大家,所以請務必在上課後與我聯絡。

在開始之前,我想感謝一些人。
這場演講中很多好的東西都是由 Austin Clements 完成的。劍橋 Go 團隊的其他成員 Russ、Than、Cherry 和 David 一直都是合作愉快、令人興奮又有趣的團隊。
我們也要感謝全球 160 萬的 Go 使用者,因為他們提供了有趣的難題讓我們解決。沒有他們,許多問題將永遠不會浮現。
最後,我要感謝 Renee French 多年來製作的所有這些可愛的山羊。你們會在整個演講過程中看到許多這些山羊。

在我們深入探討這些議題之前,我們必須展示垃圾收集器對 Go 的觀點。

首先,Go 程式有數十萬堆疊。這些堆疊由 Go 排程器管理,並始終在垃圾收集器安全點優先處理。Go 排程器會將 Go 常式多工到作業系統執行緒,希望每個硬體執行緒都能執行一個作業系統執行緒。我們藉由複製堆疊和更新堆疊中的指標來管理堆疊及其大小。這是一個區域運作,因此擴充得非常好。

另一件重要的事情是 Go 是一種值導向語言,遵循類似 C 的系統語言的傳統,而不是遵循大多數受控執行階段語言的參照導向語言的傳統。例如,這 menunjukkan tar 套件中的型別是如何佈局在記憶體中的。所有欄位都直接嵌入在 Reader 值中。這讓程式設計師在需要時能夠更有效地控制記憶體配置。它可以並置具有相關值的欄位,有助於緩存區域性。
值導向也有助於外部函式介面。我們具備適用於 C 和 C++ 的快速 FFI。顯然,Google 有大量的可用設施,但它們都是用 C++ 編寫的。Go 無法等到在 Go 中重新實作所有這些內容,因此 Go 必須透過外部函式介面存取這些系統。
這個設計決策產生了一些運作期間必須完成的更驚人的內容。這可能是 Go 與其他經過 GC 的語言最重要的區別。

當然 Go 也能有指標,事實上它們可以有內部指標。這些指標使整個值保持活躍,而且它們相當常見。

我們還有一個超越時代編譯系統,所以二進位元組包含了整個執行時間。
沒有 JIT 重新編譯。這有優缺點。首先,程式執行再現更容易,這使得編譯器改進的進展速度提高很多。
遺憾的是,我們沒有機會進行回饋最佳化,正如你使用 JIT 系統所做的那樣。
所以有優點也有缺點。

Go 附帶了兩個按鈕來控制 GC。第一個是 GCPercent。基本上這是一個按鈕,用來調整你想要使用多少 CPU,以及你想要使用多少記憶體。預設值是 100,這表示半個堆疊空間是專門用於活態記憶體,半個堆疊空間是專門用於配置。你可以朝任一方向修改這個值。
MaxHeap 目前還未發布,但已在內部使用和評估中,它讓程式設計師設定最大堆疊空間應該是什麼。記憶體不足 (OOM) 會對 Go 造成很大影響;應藉由增加 CPU 成本來處理記憶體使用量的暫時性激增,而非中止。基本上,如果 GC 偵測到記憶體壓力,它會通知應用程式,表示應釋放負載。一旦一切恢復正常,GC 會通知應用程式可以回復到其正常負載。MaxHeap 也在排程上提供了更多彈性。執行時間會將堆疊空間調整到 MaxHeap,而不是總是對可用記憶體數量感到過度焦慮。
這總結了我們對對於垃圾收集器而言很重要的 Go 部分的討論。

現在讓我們來討論 Go 執行時間,以及我們是如何到達這裡,又是如何到達目前這個位置。

現在是 2014 年。如果 Go 無法以某種方式解決這個 GC 延遲問題,那麼 Go 就無法獲得成功。這很明顯。
其他新語言也面臨相同的問題。像 Rust 這樣的語言採取了不同的方式,但我們將討論 Go 採取的路徑。
為什麼延遲如此重要?

數學在這方面完全不寬容。
99% 的隔離 GC 延遲服務層次目標 (SLO),例如 99% 的時間一個 GC 循環需要 < 10ms,根本無法擴展。重要的,是在整個工作階段或在一天內多次使用應用程式期間的延遲。假設瀏覽多個網頁的工作階段會在一個工作階段中提出 100 個伺服器請求,或者會提出 20 個請求,而且你一天會處理 5 個工作階段。在這種情況下,只有 37% 的使用者在整個工作階段中能持續體驗到低於 10ms 的延遲。
如果你希望 99% 的使用者體驗到低於 10ms 的延遲,正如我們建議的那樣,數學表示你真的需要針對 4 個 9,亦即 99.99% 的情況。
2014 年,Jeff Dean 剛發表他的論文「大規模的尾部效應」,深入探讨了這個問題。Google 內部廣泛閱讀這份論文,因為它對 Google 未來發展以及嘗試擴展到 Google 的規模會產生嚴重的後果。
我們稱此問題為「9 的暴政」。

如何對抗「9 的暴政」?
2014 年,做了很多事。
如果您想要 10 個答案,請再要求獲取更多答案,然後取前 10 個,並將這些答案放在搜尋頁面。如果要求超過 50 個百分位數,請重新發出要求或將要求轉發至另一部伺服器。如果 GC 即將執行,請拒絕新要求或將請求轉發至另一部伺服器,直到 GC 執行完畢。諸如此類。
所有這些解決方案都來自於遇到實際問題的聰明人,但他們並沒有解決 GC 延遲的根本問題。在 Google 的規模下,我們必須解決根本問題。為什麼?

冗餘無法擴展,冗餘成本很高。它需要新的伺服器農場。
我們希望我們能解決這個問題,並將此視為改善伺服器生態系統的機會,並在此過程中拯救一些瀕臨絕種的玉米田,讓一些玉米粒在 7 月 4 日長到齊膝高,並發揮其全部潛力。

以下是 2014 年的 SLO。是的,我確實低估了,我剛加入團隊,這是我的新流程,我不想做出過於樂觀的承諾。
此外,用其他語言介紹 GC 延遲簡直令人害怕。

原計畫是要進行非讀取障礙並行的複製 GC。這是長期計畫。對讀取障礙的開銷有很多不確定性,因此 Go 想避免使用它們。
但在 2014 年短期內,我們必須採取行動。我們必須將所有執行時期和編譯器轉換為 Go。當時它們是以 C 編寫的。不再使用 C,不再有因為 C 編碼人員不了解 GC 但對如何複製字串有很棒想法而造成的長尾錯誤。我們還需要一些快速且專注於延遲性的東西,但效能的影響必須小於編譯器提供的加快速度。所以我們受到限制。我們基本上有一年時期的編譯器效能改善,我們可以透過讓 GC 並行運行來使用完畢。但僅此而已。我們無法減慢 Go 程式速度。這在 2014 年是不可行的。

所以我們收斂了一點。我們不會進行複製部分。
決定使用三色並行演算法。在我的職業生涯中,Eliot Moss和我研究出讓Dijkstra演算法能用於多個應用程式執行緒的期刊文件證明。我們也證明了我們可以解決STW問題,並且我們已找到可以這麼做的證明。
我們也擔心編譯器速度,也就是編譯器產生的程式碼。如果我們讓寫入屏障在大部分時間都關閉,編譯器最佳化就會受到最小影響,而編譯器團隊可以快速前進。Go也在2015年非常需要短期的成功。

因此,讓我們來看一下我們做的一些事。
我們使用大小隔離區間。內部指標是有問題的。
垃圾收集器需要有效地找到物件的開頭。如果它知道區間中物件的大小,它就只需向下取至該大小,這樣就是物件的開頭。
當然,大小隔離區間還有一些其他優點。
低片段化:除了Google的TCMalloc和Hoard之外,使用C的經驗,我和英特爾的可擴充套件Malloc有著密切的關聯,那項工作讓我們相信非移動式分配器不會產生片段化問題。
內部結構:我們完全瞭解它們,並且有相關經驗。我們瞭解如何執行大小隔離區間,我們瞭解如何執行低爭用或零爭用分配路徑。
速度:非複製不讓我們擔心,分配可能會較慢,但仍介於C的順序。它可能沒有碰觸指標那麼快,但沒關係。
我們也有這個外部函式介面問題。如果我們沒有移動物件,那麼我們就不必處理變動收集器在您嘗試固定物件並在C和您正在處理的Go物件之間放置間接層級時可能會遇到的後續錯誤。

下一個設計選擇是放置物件的元資料。由於我們沒有標頭,所以我們需要具有一些關於物件的資訊。標記位元保留在側邊,並用於標記和分配。每個字詞都有2個位元與之關聯,告知您它在該字詞中是純量還是指標。它也編碼物件中是否還有更多指標,以便我們能及早停止掃描物件。我們還有一個額外的編碼位元,我們可以將其用作額外的標記位元,或執行其他除錯動作。這對於讓這些東西執行並找出錯誤非常有價值。

那寫入屏障又是如何呢?寫入屏障僅在垃圾回收程式執行期間啟動。在其他期間,編譯程式會載入全域變數並檢視變數。由於垃圾回收程式通常會關閉,硬體會正確推測出迴避寫入屏障的分支。當我們在垃圾回收程式內部,變數會有所不同,而寫入屏障負責確保不會在三原色作業期間遺失任何可存取的物件。

這段程式碼的另一部分為垃圾回收程式步調器。那是 Austin 出色成果之一。它的基礎基本上是反饋迴圈,用於決定在何時最佳啟動垃圾回收程式週期。如果系統處於穩態且未處於相位變化中,則標記作業將在用完記憶體時結束。
情況可能並非如此,因此步調器還必須監控標記進度,並確保配置不會超過同時標記的進度。
在需要時,步調器會降低配置速度,同時加快標記速度。步調器會在高階層級停止大量配置的 Goroutine,並使其執行標記工作。工作量與 Goroutine 的配置成正比。這樣一來垃圾回收程式會加快速度,而變異器會降低速度。
完成所有作業後,步調器會藉由這個垃圾回收程式週期以及先前的週期所得的資訊預測下次啟動垃圾回收程式時間。
它的功能遠超過以上說明,但那是基本方法。
數學運算絕對引人入勝,請傳訊息給我取得設計文件。如果你正在執行同時垃圾回收,你真的有必要查看這些數學運算並確認是否與你的數學運算相同。如果你有任何建議,請讓我們知道。
*Go 1.5 同時垃圾回收程式步調器 和 提案:設定目標軟最大堆積量及硬最大堆積量

是的,我們獲得成功,而且有許多成功。年輕且瘋狂的 Rick 會將其中一些圖表刺青在自己的肩膀上,因為他對這些圖表感到非常自豪。

這是一系列針對 Twitter 生產伺服器執行的圖表。我們當然與那個生產伺服器毫無關聯。Brian Hatfield 執行這些測量,並意外地在推文中提到這些測量結果。
Y 軸上以毫秒為單位顯示垃圾回收器延遲時間。X 軸上以時間為單位。各個點表示垃圾回收器期間停止世界的暫停時間。
我們在 2015 年 8 月的首次發表版本中,發現延遲時間從大約 300 - 400 毫秒下降到 30 或 40 毫秒。這很好,在數量級上來說很好。
我們要徹底地從 0 到 400 毫秒調整 Y 軸,再從 0 到 50 毫秒調整 Y 軸。

這是六個月後。此項改善主要歸功於系統性地移除所有在停止世界時間中所做的 O(heap) 事項。這是我們第二個量級的改善,從 40 毫秒降至 4 或 5 毫秒。

其中有些我們必須清除的錯誤,而我們在小版本 1.6.3 中完成了這項工作。這將延遲降至低於 10 毫秒,這是我們的 SLO。
我們打算再次變更我們的 Y 軸,這次降至 0 至 5 毫秒。

因此我們來到這裡,這是 2016 年 8 月,距離首次發布已過了一年。我們再次持續淘汰這些 O(堆積大小) 停止世界的程序。我們在這裡討論的是 18GB 的堆積。我們有更大的堆積,當我們淘汰這些 O(堆積大小) 停止世界的暫停,堆積的大小顯然可以在不影響延遲的情況下大幅增加。因此這在 1.7 中有所助益。

下一個版本是在 2017 年 3 月。我們有了最後的大幅延遲下降,這是因為找出如何在 GC 周期的末尾避免停止世界堆疊掃描。這將我們降至毫秒以下的範圍。Y 軸再次即將變更為 1.5 毫秒,而我們看到第三個量級的改善。

2017 年 8 月的版本只看到小幅改善。我們知道是什麼原因造成其餘的暫停。此處的 SLO 耳語數字大約在 100-200 微秒之間,而我們將朝這個目標推進。如果您看到超過幾百微秒的情況,那麼我們真的想與您討論並找出這是否符合我們已知的內容,或者這是我們之前未曾查明的新事物。無論如何,似乎對於較低的延遲需求不大。重要的是要注意,這些延遲等級會發生於各種非 GC 原因,俗話說:「你不需要比熊快,你只需要比你旁邊的人快。」
2 月的 1.10 版沒有實質性的變更,只有一些清除和處理邊界情況。

因此,迎向新的一年和新的 SLO。這是我們 2018 年的 SLO。
我們已將 GC 週期中使用的總 CPU 降至 CPU。
堆積仍維持在 2 倍。
我們現在的目標是每個 GC 週期 500 微秒停止世界暫停。這裡可能有點杞人憂天。
分配量將持續與 GC 協助成比例。
Pace 已大幅改善,因此我們希望在穩態中看到最少的 GC 協助。
我們對此相當滿意。這也不是 SLA 而是 SLO,所以它是一個目標,而不是一項協議,因為我們無法控制作業系統等事物。

那是好東西。讓我們轉換主題,開始討論我們的失敗。這些是我們的傷疤;它們就像刺青,而且每個人都有。無論如何,它們附帶了更好的故事,所以讓我們講一些那樣的故事。

我們的第一次嘗試是做一些稱為請求導向收集器或 ROC 的事情。可以在這裡看到該假設。

它的意思是什麼呢?
Goroutine 是看起來像土撥鼠的輕量級執行緒,所以我們在此有兩個 Goroutine。它們共享某些東西,例如中間的兩個藍色物件。它們有自己的私人堆疊和自己選擇的私人物件。假設左邊的人想要共享綠色物件。

Goroutine 將它放入共享區域,以便另一個 Goroutine 可以存取它。它們可以將它掛接到共享堆中的東西,或將它指定給全域變數,而另一個 Goroutine 能夠看到它。

最後,左邊的 Goroutine 走向死亡,它即將死亡,令人難過。

您知道死亡時無法帶走您的物件。您也不能帶走您的堆疊。堆疊在此刻實際上是空的,物件也無法存取,因此您可以直接回收它們。

這裡的重要之處是,所有動作都是本地的,並且不需要任何全域同步。這與類似世代 GC 的方法有根本上的不同,希望我們從必須執行該同步作業中獲得的縮放,足以讓我們獲勝。

這個系統發生的另一個問題是寫入屏障總是開啟的。每當有寫入時,我們必須查看它是否將私有物件的指標寫入公用物件。如果是,我們必須使被控物件公開,然後執行可存取物件的遞移巡覽,確保它們也公開。那是相當昂貴的寫入屏障,可能會導致許多快取遺漏。

話雖如此,哇,我們獲得了一些相當不錯的成功。
這是一個端對端的 RPC 基準測試。錯誤標示的 Y 軸從 0 到 5 毫秒(小於較佳),無論如何,它就是這樣。X 軸基本上是壓艙物或核心資料庫有多大。
如您所見,如果您啟用 ROC 且沒有太多共享,事項實際上能相當良好縮放。如果您沒有啟用 ROC,它的表現沒有那麼好。

但這還不夠,我們還必須確保 ROC 沒有讓系統中的其他部分變慢。當時,我們的編譯器引起我們不少的憂慮,而我們不能讓編譯器變慢。很不幸的是,編譯器正是 ROC 無法處理好的程式。我們看到程式執行速度變慢了 30%、40%、50% 以上,這是無法接受的。Go 以其編譯器執行速度快而自豪,所以我們不能讓編譯器變慢,絕對不能慢到這種程度。

接著,我們檢視了一些其他程式。這些是我們的效能基準測試。我們有一組 200 或 300 個基準測試,編譯器人員決定,這些基準測試對他們而言很重要,他們必須進行改善。這些基準測試並不是 GC 人員選出來的。各項數字都很糟糕,而且 ROC 並沒有因此而勝出。

的確,我們擴充了系統規模,但我們只有 4 到 12 個硬體執行緒系統,因此無法克服寫入障礙稅。也許在未來,當我們有 128 個核心系統,而且 Go 能善加利用這些核心系統時,ROC 的擴充特性或許會奏效。到時候,我們再來重新檢視此事;但就目前而言,ROC 是個失敗的策略。

好,接下來我們要做甚麼?我們來試試代間 GC。它是一個老掉牙、但還不錯的策略。ROC 不管用,所以我們回到我們有最多經驗的東西。

我們不會放棄我們的延遲,我們不會放棄我們沒有移動的事實。所以,我們需要一個沒有移動的代間 GC。

我們能做到這件事嗎?可以,但使用代間 GC 時,寫入障礙始終開啟。當 GC 循環執行時,我們使用與今天相同的寫入障礙;但當 GC 關閉時,我們會使用一個快速的 GC 寫入障礙,這個寫入障礙會將指標暫存,然後在指標溢位時將緩衝資料沖刷到卡標記表中。

那麼,這如何在沒有移動的情況下運作呢?以下是標記/配置對應表。基本上,您維持一個目前的指標。當您配置時,您會尋找下一個 0,當您找到 0 時,您會在那一個空間中配置一個物件。

然後,您會將目前的指標更新為下一個 0。

您會繼續執行,直到某個時候需要執行一代 GC。您會注意到,如果標記/配置向量中有一個 1,則代表那個物件在最後一次 GC 時還存在,所以它已成熟。如果它是 0,而您找到了它,那麼您就知道它還很年輕。

那麼,您如何執行晉升?如果您發現一個以 1 標記的東西指向一個以 0 標記的東西,那麼您只要將那個 0 設定為 1 即可晉升引用項。

必須執行傳遞式巡訪,才能確保促成所有可及物件。

當所有可及物件都已促成,則次要 GC 終結。

最後,若要完成世代型 GC 循環,只需將目前指標設定回向量的開始,便可繼續進行。所有零在該 GC 循環中皆未可及,因此可用來重複使用。許多人知道這稱為「附著位元」,是由漢斯.伯姆及其同事所發明。

那麼,效能表現如何?對於大型堆積來說,表現並不算差。這是 GC 應於其上表現良好基準。這一切都好。

接下來,我們於效能基準上執行相關操作,情況不如預期。那發生了什麼事?

寫入屏障雖然快,但還是不夠快。而且,它很難進行最佳化。例如,若於配置物件時至下一個安全點之間進行初始化寫入,則可省略寫入屏障。但是,我們必須移轉至系統,系統於每個指令設有 GC 安全點,所以我們無法再省略任何寫入屏障。

我們也進行逃逸分析,結果愈來愈好。還記得我們談論過的重視價值的事嗎?我們傳遞實際值,而不是將指向函式的指標傳遞出去。因為傳遞值,逃逸分析只需執行程序內逃逸分析,而無需執行程序間分析。
當然,如果是指向本地物件的指標有溢位狀況,則物件必須堆積配置。
世代假說並不適用於 Go,這不是假說的錯,而是因為小物件在堆疊中的存活與結束時間都較短。因此,其他受控執行時間語言中的世代收集效率會比 Go 高出許多。

因此,對抗寫入屏障的力量開始逐漸累積。如今,我們的編譯器已遠比 2014 年時優秀。逃逸分析抓出很多這些物件,並將它們放置於堆疊物件中,這些是世代收集器將協助完成的工作。我們開始建立工具,協助使用者找出表示溢位的物件,如果溢位狀況輕微,使用者可以變更程式碼,並協助編譯器於堆疊中配置。
使用者愈來愈靈巧地採取價值導向的方式,而且指標數量也減少了。陣列和對應保留值,而非指向結構的指標。一切都好。
但那並非造成未來寫入屏障在 Go 中舉步維艱的主要原因。

讓我們來看這張圖表。這只是記錄標記成本的分析圖表。每條線代表可能具有標記成本的不同應用程式。假設您的標記成本是 20%,這相當高,但有可能。紅線是 10%,仍然很高。底線是 5%,大約是現今寫入障礙的成本。如果您將堆大小加倍,會發生什麼事?那就是右側的點。由於 GC 週期較不頻繁,因此標記階段的累積成本會大幅下降。寫入障礙的成本是固定的,因此增加堆大小的成本會將該標記成本降至寫入障礙的成本之下。

以下是寫入障礙的更常見成本,為 4%,而且我們看到即使這樣,我們也可以透過單純地增加堆大小,將標記障礙的成本降到寫入障礙的成本之下。
代間 GC 的真正價值在於,在查看 GC 時間時,會忽略寫入障礙的成本,因為它們會抹在變異器上。這是代間 GC 的最大優勢,它大幅降低了完整 GC 週期的長時間停止全世界的時間,但並非一定會改善處理量。Go 沒有這個停止全世界的問題,因此它必須更仔細地檢視處理量問題,而這就是我們所做的。

這是很大的失敗,且伴隨著這種失敗而來的是食物與午餐。我發出我慣常的抱怨:「哎呀如果沒有寫入障礙,這不就會很棒嗎?」
同時,奧斯汀才剛花了一小時與 Google 的一些硬體 GC 人員交談,他表示我們應該與他們交談,並試圖找出如何取得可能有所幫助的硬體 GC 支援。然後我開始述說關於零填充快取行、可重新啟動原子序列以及其他在我為一家大型硬體公司工作時未被採用的事物的戰爭故事。當然,我們將一些東西塞進稱為 Itanium 的晶片中,但我們無法將它們塞進今日更流行的晶片中。因此,這個故事的寓意就是要善用我們擁有的硬體。
無論如何,那讓我們興起了談話,一些瘋狂的事怎麼樣?

沒有寫入障礙的卡片標記怎麼樣?結果是奧斯汀有這些檔案,他將他所有的瘋狂點子寫進這些檔案中,而基於某種原因,他沒有告訴我。我認為這是一種治療。我曾經與艾略特一起做同樣的事情。新點子很容易被砸碎,而人們需要在將其發布到世界上之前保護它們並使其更強大。好吧,無論如何,他提出了這個點子。
其概念是針對每個卡維護一個成熟指標的雜湊。如果指標寫入卡片,雜湊會變更,且該卡片會被視為已標記。此舉會以雜湊成本取代寫入監控的成本。

但更重要的是它與硬體對齊。
當今的現代架構具有 AES(進階加密標準)指令。其中一個指令可以進行加密等級雜湊,而且藉由加密等級雜湊,如果我們也遵循標準加密政策,就不用擔心衝突。因此,雜湊不會花費我們太多成本,但我們必須載入要雜湊的內容。很幸運的是,我們會循序巡覽記憶體,這樣我們就能獲得非常好的記憶體和快取效能。假設你有一個 DIMM,並且你會命中順序位址,這將產生優於隨機命中位址的效益,因為速度會更快。硬體預先擷取器會啟動,這也有幫助。無論如何,我們有 50 年、60 年的硬體設計經驗,用於執行 Fortran、C,以及執行 SPECint 基準測試。結果正是硬體以此類型的快速執行,這樣的結果並不令人意外。

我們執行量測了。這還不錯。這是針對大型堆疊的基準測試套件,效果應該不錯。

接著我們表示它在效能基準測試中會是什麼樣子?並不是非常好,有一些異值。但現在,我們已經將寫入監控從在變動器中持續開啟,改為作為 GC 循環的一部分。現在,關於我們是否要執行世代 GC 的決定已延後至 GC 循環開頭。我們在此有更多的控制權,因為我們已經將卡片工作區域化了。現在我們有了工具,就可以將其轉交給 Pacer,而且它可以動態地切斷落在右側、不會從世代 GC 中受益的程式,並做好工作。但是,這樣的作法是否就能持續獲利呢?我們必須知道,或至少思考一下未來的硬體會是什麼樣子

未來的記憶體會是什麼樣子?

讓我們看看這個圖形。這是一個典型的摩爾定律圖形。Y 軸上是對數刻度,顯示單晶片中的電晶體數量。X 軸是 1971 年至 2016 年之間的年數。我特別注意,這些是某人在某個地方預測摩爾定律已經死亡的年份。
十年前左右,德納德縮放已結束了頻率改善。新製程需要更長的時間來提升。因此,現在變成四年或更長的時間,而不是兩年。顯然,我們正在進入摩爾定律放緩的時代。
讓我們僅看紅圈中的晶片。這些晶片最能維持摩爾定律。
這些晶片的特點是邏輯越來越簡單,並多次重複使用。許多相同的核心、多個記憶體控制器和快取、GPU、TPU 等。
隨著我們持續簡化和增加重複,我們最後只會得到幾條導線、一個電晶體和一個電容。換句話說,就是一個 DRAM 記憶體儲存格。
換個說法,我們認為加倍記憶體將比加倍核心更有價值。
原始圖表取自 www.kurzweilai.net/ask-ray-the-future-of-moores-law。

讓我們看看另一張專注於 DRAM 的圖表。這些數字來自 CMU 最近的一篇博士論文。從中可以看到,摩爾定律是藍色線條。紅色線條是容量,而且看來符合摩爾定律。奇怪的是,我看到一張圖表回溯到 1939 年,當時我們使用的是鼓式記憶體,而容量和摩爾定律當時就已經一起推動著進步,因此這張圖表的歷史很悠久,肯定比在座的每一個人還久。
若將此圖表與 CPU 頻率或各種關於摩爾定律已死的圖表比較,我們會得出以下結論:記憶體,或至少晶片容量,將比 CPU 更久符合摩爾定律。頻寬(黃色線條)不僅與記憶體頻率有關,還與可以從晶片取得的針腳數有關,因此跟不上,但表現還算不錯。
延遲時間(綠色線條)表現很糟,不過我要指出,循序存取的延遲時間比亂數存取的延遲時間表現更好。
(資料來自「理解並改善基於 DRAM 的記憶體系統的延遲時間,以部分滿足電機與電腦工程博士學位要求,Kevin K. Chang,電機與電腦工程碩士,卡內基美隆大學,電機與電腦工程學士,卡內基美隆大學,卡內基美隆大學,賓夕法尼亞州匹茲堡,2017 年 5 月」。請參閱 Kevin K. Chang 的論文。引言中的原始圖表不適合我輕鬆地繪製摩爾定律線條,因此我將 X 軸改為較為均勻)。

讓我們進入正題探討。這是實際的 DRAM 價格,而且通常從 2005 年到 2016 年已經下降。我選擇 2005 年,因為這是大約在 Dennard 縮放結束的時候,而與此同時,頻率也得到了改善。
如果查看紅色的圓圈,這基本上是指我們進行減少 Go 的 GC 延遲工作的那段時間,我們會看到前幾年價格很好。最近則不然,因為需求已經超過供應,導致價格在最近兩年內上漲。當然,電晶體並沒有變大,在某些情況下,晶片產能已經增加,因此這是由市場力量驅動的。RAMBUS 和其他晶片製造商表示,未來我們將看到我們的下一個流程縮減出現在 2019-2020 年的時間範圍內。
我將避免推測記憶產業中的全球市場力量,除此之外,價格是週期性的,而長期而言,供應往往會滿足需求。
長期而言,我們相信記憶體價格將以遠快於 CPU 價格的速度下降。
(來源 https://hblok.net/blog/ 和 https://hblok.net/storage_data/storage_memory_prices_2005-2017-12.png)

讓我們看看這條其他線。如果我們在這條線上,那會很好。這是 SSD 線。它在維持低價格方面做得更好。這些晶片的材料物理特性比 DRAM 複雜得多。邏輯更為複雜,每一個儲存單元一個電晶體變成大概半打左右。
未來 DRAM 和 SSD 之間有一條線,NVRAM 如英特爾的 3D XPoint 和相變化記憶體 (PCM) 將部署於此。在未來十年內,這種型式的記憶體可用性增加可能會變得更為主流,並且這只會強化這種想法:新增記憶體是為我們的伺服器增加價值的廉價方式。
更重要的是,我們可以預期看到其他與 DRAM 競爭的替代方案。我假裝不知道五年或十年內哪個會受青睞,但競爭將會很激烈,而堆疊式記憶體將會更貼近這裡突顯的藍色 SSD 線。
這一切強化了我們為了增加記憶體而避免持續存在屏障的決定。

那麼,這一切對 Go 的未來而言是什麼意思呢?

當我們查看使用者提出的臨界案例時,我們打算讓執行時期更靈活、更健全。我們希望限制行程編輯器並獲得更好的確定性和公平性,但我們不想犧牲任何我們的效能。
我們亦無意增加 GC API 的用量。過去近十年,我們有兩個控制項,感覺上這個數量剛剛好。目前沒有哪個應用程式重要到讓 Google 為它新增一個新標誌。
我們亦會研究如何改善我們已經相當不錯的逃逸分析並針對 Go 的面向值程式設計進行最佳化。不只是在程式設計,還要包括我們提供給使用者的工具。
在演算法上,我們會專注於設計空間中可盡量減少使用屏障的部分,尤其是那些始終開啟的屏障。
最後,也是最重要的一點,我們希望搭上摩爾定律偏愛 RAM 勝於 CPU 的趨勢,至少在接下來的 5 年,最好是接下來的十年。
好,就這樣。謝謝。

附註:Go 團隊正在尋找工程師來協助開發和維護 Go 執行時期和編譯器工具鏈。
有興趣嗎?歡迎瀏覽我們的 公開職缺。
下一篇: 使用 Go Cloud 進行可移植雲端程式設計
上一篇: 更新 Go 行為準則
部落格索引