Go performance from version 1.0 to 1.22

Ben Hoyt

Go 1.0から1.22までのパフォーマンス

原文は Ben Hoyt により に公開されました。 このブログを購読する

2年前に、私は比較を行いました。自作のGoAWKインタプリタについて、Go 1.2から1.18までのすべてのバージョンで2種類のベンチマークを実行したものです。

本記事では、欠けていたバージョン(1.0と1.1)と新しいバージョン(1.19から1.22)を加えて、それらのベンチマークを再実行します。また、Go 1.20で追加されたプロファイルガイド最適化(PGO)を有効にした場合の結果も掲載します。セットアップを理解するために以前の記事を読み直す必要がないよう、元の記事からかなり多く引用しています。

Goで書かれたプログラムが高速化された理由はさまざまです。Goチームや外部のコントリビューターがコンパイラを改善し、ランタイムやガベージコレクタ、標準ライブラリを最適化してきました。ここでは、Go 1.0から執筆時点で最新の1.22までにリリースされた各バージョンでコンパイルした際のGoAWKのパフォーマンスを比較します。

検証のため、AWKでできることの両極端を表す2つのAWKプログラムでGoAWKを実行しました。1つは文字列処理を伴うI/O、もう1つは数値計算です。

まず1つ目はcountwordsです。入力中の単語の出現回数を数え、単語とそのカウントを出力する文字列処理タスクで、AWKスクリプトとしては典型的なものです。入力には欽定訳聖書を10回連結したものを使いました(以前パフォーマンス比較で使ったものです)。コードは次のとおりです。

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

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

2つ目のプログラムはsumloopで、ループカウンタを変数に何度も加算するタイトなループです。AWKの典型的な使い方とは言えませんが、GoAWKのバイトコードインタプリタのループをテストするには良い題材です。

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

古いGoのバージョンでもコンパイルできるように、GoAWKのコードを少し修正する必要がありました。特にGo 1.0にはbufio.Scannerが存在せず、GoAWKではそれを多用しているためです。1.0についてはGo 1.1のbufio.Scannerの実装を利用しました。

グラフの計測値は、私のx86-64 LinuxノートPC上での実行時間(秒、3回実行したうちの最速)です。青い線がcountwords、赤い線がsumloopです(ちなみに、前回は結果のラベルを取り違えていました)。なお、今回は最近のバージョンでのより微細な改善を分かりやすくするため、Y軸は対数スケールになっています。

グラフには各GoバージョンでビルドしたGoAWKのバイナリサイズも併せて載せています。薄い灰色の線がそれです。

今回も、すべてを実行して時間を計測するためにPythonスクリプトを使いました。結果のグラフはこちらです(表形式でもご覧いただけます)。

GoバージョンごとのGoAWKの速度

最も大きな改善はバージョン1.3、1.5、1.7、そして1.12で見られます。それ以降は非常に緩やかな高速化にとどまっており、手軽に得られる成果はとうに刈り取られています。

今回はGo 1.2でcountwords奇妙な性能低下が見られました。1.1では7.5秒だったのが1.2では25.5秒に跳ね上がり(!)、そして1.3では2.8秒まで下がっています。これはほぼ確実にスタックの「ホットスプリット」問題が原因で、Goチームが「goroutineスタックの実装を従来の『セグメント化』モデルから連続モデルへ変更した」ことにより1.3で修正されました

プロファイリングによって、実行時間の大部分をランタイムのスタック操作が占めていることに気づき、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」を使用して翻訳されました。

コメント