Go performance from version 1.0 to 1.22

Ben Hoyt

從 Go 1.0 到 1.22 的效能表現

原文由 Ben Hoyt 發布,訂閱此部落格

兩年前,我曾比較了我的 GoAWK 直譯器在 Go 1.2 到 1.18 各個版本上的兩項不同效能測試。

在這篇文章中,我重新執行了這些測試,補上了先前缺少的 Go 版本(1.0 和 1.1),也加入了新版本(1.19 到 1.22)的結果。我也包含了啟用設定檔導引最佳化(PGO)的測試結果,這項功能是在 Go 1.20 加入的。為了讓你不用回頭重讀舊文也能了解測試的設定,我會大量引用原文的內容。

用 Go 撰寫的程式變快的原因有很多:Go 團隊與外部貢獻者持續改進編譯器,並對執行環境、垃圾回收機制和標準函式庫進行了最佳化。在此,我們比較使用從 1.0 到 1.22(撰寫本文時的最新版本)各個已發布的 Go 版本來編譯 GoAWK 時的效能表現。

我是透過在 GoAWK 上執行兩個 AWK 程式來進行測試,這兩個程式代表了 AWK 應用的兩個極端:包含字串處理的 I/O,以及數值運算。

首先是 countwords,這是一個字串處理任務,會統計輸入資料中各個單字的出現頻率,並印出單字與其次數。這是 AWK 腳本的典型應用。輸入資料是《英王欽定本聖經》重複串接 10 次後的版本(我之前在效能比較中也曾使用過)。以下是程式碼:

{
    for (i=1; i<=NF; i++)
        counts[tolower($i)]++
}

END {
    for (k in counts)
        print k, counts[k]
}

第二個程式是 sumloop,這是一個緊湊的迴圈,會重複將迴圈計數器累加到一個變數中好幾次。這個程式並非 AWK 的典型用法,但很適合作為 GoAWK bytecode 直譯器迴圈的測試:

BEGIN {
    for (i=0; i<10000000; i++)
        sum += i+i+i+i+i
}

為了讓 GoAWK 的程式碼能在較舊的 Go 版本上編譯,我稍微調整了一下程式碼。特別是針對 Go 1.0,因為它還沒有 bufio.Scanner,而 GoAWK 大量使用了它。我在 1.0 上使用的是 Go 1.1 版 bufio.Scanner 的實作。

圖表中的時間數據是我在 x86-64 Linux 筆電上測得的秒數(取三次執行中的最佳成績)。藍線是 countwords,紅線是 sumloop(順帶一提,上次我把結果標示錯了)。請注意,這次 Y 軸採用對數刻度,以便更清楚地呈現近年版本中較細微的改進。

圖表中也包含了各個 Go 版本所編譯出的 GoAWK 執行檔大小——也就是淺灰色的那條線。

和上次一樣,我使用一個Python 腳本來執行所有測試並測量時間。以下是圖表(如果你偏好表格,也可以看表格版):

GoAWK 在各 Go 版本上的速度表現

最顯著的效能提升出現在 1.3、1.5、1.7 和 1.12 版。在此之後,速度的提升就非常緩慢了——所有唾手可得的優化早就已經完成了。

這次 countwords 在 Go 1.2 出現了奇怪的效能暴增:執行時間從 1.1 版的 7.5 秒暴增到 1.2 版的 25.5 秒(!),然後在 1.3 版又降回 2.8 秒。這幾乎可以確定是由堆疊「hot split」問題所造成,該問題在 1.3 版中已被修復,當時 Go 團隊將「goroutine 堆疊的實作從舊有的『分段式』模型改為連續式模型」。

我是透過效能分析找出 1.2 版異常的原因,並注意到執行階段的堆疊操作佔了執行時間的極大比例。以下是 pprof 輸出的前幾行:

$ go tool pprof --text ./goawk_1.2 go12.prof 
Total: 1830 samples
     332  18.1%  18.1%      332  18.1% runtime.newstack
     296  16.2%  34.3%      296  16.2% runtime.memclr
     281  15.4%  49.7%      281  15.4% runtime.oldstack
     222  12.1%  61.8%      619  33.8% github.com/benhoyt/goawk/interp.(*interp).execute
      91   5.0%  66.8%       91   5.0% runtime.lessstack
      75   4.1%  70.9%      133   7.3% github.com/benhoyt/goawk/interp.(*interp).callBuiltin
      57   3.1%  74.0%       57   3.1% runtime.stackfree
      53   2.9%  76.9%       81   4.4% strings.FieldsFunc
      ...

啟用 PGO 後效能僅提升了幾個百分點,以 Go 1.22 來說,countwords 約提升 2%,sumloop 約提升 7%。我發布的 GoAWK 執行檔都是以啟用 PGO 的方式編譯的。

執行檔大小這些年來一直保持相當穩定,除了在 1.2 版有一次大幅增加。即使啟用 PGO,執行檔也只會大約增加 5%,所以我認為通常還是值得啟用的。

整體而言,countwords 現在的速度約為使用 Go 1.0 時的 8 倍,而 sumloop 則快了 24 倍。感謝 Go 團隊這些年來的辛勤付出!

本文章由 muse-spark-1.2-contributor 進行翻譯

留言