Golang泛型是更快了還是慢了? - DoltHub

banq發表於2022-04-02

Go 1.18 已經發布,隨之而來的是對泛型的期待已久的支援!泛型是多年來語言最重大的變化。它們為原本極簡的型別系統增加了一個新維度。
當初一開始,Golang就通過 “介面”支援動態多型;泛型現在為Golang提供靜態多型性。
今天我們將討論 Golang 對泛型的實現以及它對 CPU 和記憶體利用率的意義。

泛型很慢?
在這個重要的版本釋出之後,出現了一波關於Go中泛型變慢的討論,以及它們將如何影響現有的Go專案。
到目前為止,最好的分析是Vincent Marti寫的這個深度分析。如果你還沒有看過,我強烈建議你看一下。
該文章的主要重點是Go編譯器如何實現泛型,以及這些設計決策如何對泛型Go程式碼的效能產生負面影響。
該文章最後總結了Golang泛型的最佳實踐:其中包括 "不要重寫基於介面的API來使用泛型"。

單態化
在大多數常用的語言中,靜態多型性和動態多型性從實現的角度看沒有什麼共同之處。

  1. 動態多型性最常使用虛擬方法表(簡稱vtables)來實現,在執行時動態解決方法呼叫。
  2. 靜態多型性通常完全是在編譯時實現的,為每個用於呼叫多型性函式的型別生成一個新版本,這個過程被稱為單態化。

在實現泛型時,Golang團隊選擇了一條中間道路,他們稱之為 "字典和Gcshape Stenciling"。
它是靜態單態化("模版化")和通過vtables("字典")動態呼叫的結合。編譯器不是為每個用於呼叫函式的型別編制一個新的函式,而是按 "gcshape "分組呼叫型別,併為每個gcshape生成一個函式的副本。
一般來說,值型別都有一個獨特的gcshape,但引用型別(指標和介面)都共享一個gcshape:

當你在Golang中把引用型別傳遞給泛型函式時,靜態多型性變成了動態函式排程。

這一設計決定的後果是對效能產生了重大影響。這就是為什麼Vincent Marti的文章得出結論說泛型會使你的程式碼變慢的原因。
它的分析特別關注引用型別,並詳細解釋了當它們與泛型結合使用時,效能如何以及為什麼會下降。
然而,討論中缺少的是泛型與值型別的互動。

泛型是快速的?
我們對泛型的工作原理有了一定的瞭解,而且我們知道它們並沒有為引用型別提供任何明顯的效能優勢。
所以現在的問題是,對於值型別來說,情況是否有任何不同?
我以前寫過關於值型別的效能,以及它們如何比引用型別更好地配合Golang的記憶體模型。長話短說,值型別更容易減少記憶體分配,因為Go編譯器的轉義分析幾乎總是將引用型別放在堆上。

.....更多點選標題

總結
我們已經找到了我們的答案:使用帶有值型資料結構的演算法的通用實現讓我們把資料保留在棧上,避免了記憶體分配的開銷。
我們的引用型別的資料結構會逃到堆中,而不管使用的是什麼函式。這並不是嚴格意義上的必要,因為我們在這裡建立的引用只在堆疊中傳遞,而不是在堆疊中傳遞。
然而,編譯器的轉義分析沒有利用這一事實,而是將其放在堆上。
類似地,我們將我們的值型別資料結構傳遞給函式的介面實現,我們隱含地將它轉換為引用型別,然後它就逃逸到了堆中。
只有當我們把值型別傳遞給它的通用函式的stenciled副本時,編譯器才會避免這種分配。

靜態多型性通常被認為是一種面向效能的特性。就像強型別化一樣,它給編譯器提供了額外的資訊,為生成程式碼時進行更積極的優化提供了機會。這在像C++這樣將泛型函式完全單態化的語言中當然是真的。但就像語言本身一樣,Golang的泛型並不遵循既定的規則,即事情應該是怎樣的。

Golang的泛型函式實際上可以幫助提高效能,但也不是你所期望的那樣。編寫高效能的Go程式碼通常意味著將介面型別限制在高層結構中,這正是我們前面看到的原因:它們與轉義分析不相容,而且幾乎總是導致昂貴的記憶體分配。這使得程式設計師只能使用具體型別,而沒有多型性的設施。泛型可能是這個問題的一個答案。

泛型是Golang的一個全新的特性,對它們的支援只會隨著時間的推移而改善。我們今天的討論完全集中在當前的實現上,但在語言規範中沒有任何東西可以阻止當前設計的改變。

相關文章