Go performance from version 1.2 to 1.18

Ben Hoyt

Go 1.2から1.18までのパフォーマンス

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

最近、私はGoで書いたAWKインタプリタであるGoAWKのパフォーマンスを、木構造をたどるインタプリタからバイトコードコンパイラと仮想マシンインタプリタへの切り替えによって改善しました。

その作業の中で、Go自体のパフォーマンスがこの数年でどれだけ向上したのかを調べてみると面白いのではないかと思いました。

Goで書かれたプログラムが高速化した理由はさまざまです。Goチームや外部のコントリビューターがコンパイラを改善し、ランタイムやガベージコレクタ、標準ライブラリを最適化してきました。以下では、GoAWKをGoの各リリースバージョンでコンパイルした際のパフォーマンスを比較します。対象は、私がダウンロードできた最も古いバージョンである1.2から、現在ベータ版の1.18までです。

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

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

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

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

2つ目のプログラムはsumloopで、ループカウンタを変数に何度も加算するタイトなループです。こちらはあまり典型的なAWKの使い方ではありません。

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

最初のグラフの数値は、私のx86-64 LinuxノートPCでの実行時間(秒、3回実行したうちの最良値)です。あわせて、GoのバージョンごとのGoAWKバイナリサイズのグラフも掲載しています。

すべてのバージョンでの実行と計測にはPythonスクリプトを使いました。結果のグラフは次のとおりです(表形式でもご覧いただけます)。

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

GoのバージョンごとのGoAWKバイナリサイズ

Go 1.3の時点で、かなり分かりやすい改善余地が摘み取られたのでしょう。リリースノートによると、ランタイムやガベージコレクタ、スタックの扱いに大きな変更があったとのことです。その後、countwordsはGo 1.7まで、sumloopは1.9まで着実に高速化し、それ以降は現在の1.18に至るまで緩やかな改善が続いています。

詳しく掘り下げたわけではありませんが、私の推測では(少なくとも1.3以降の)countwordsの改善は主に標準ライブラリの改善によるもので、一方でCPUバウンドなsumloopの改善はコンパイラの最適化によるものです。

最近の改善の1つがバージョン1.17での変更で、関数の引数や戻り値をスタックではなくレジスタで渡すようにしたものです。これはGoAWKの旧来のtree-walkingインタプリタにとって大きな改善で、あるマイクロベンチマークでは38%の高速化、すべてのマイクロベンチマークの平均でも17%の高速化を確認しました

興味深いことに、GoAWKの新しい仮想マシン実装では、引数をレジスタで渡す変更による目立った改善はありません。これは、仮想マシンが異なるオペコードをディスパッチするために大きなswitch文を使っている一方で、関数呼び出し自体はほとんど使っていないためです。対照的に、tree-walkingインタプリタでは式の評価ごとにevalへの(再帰的な)関数呼び出しが必要だったため、大きなパフォーマンス向上が得られていました。

クリックすると旧tree-walkingインタプリタでのベンチマーク結果が表示されます()。全体的な傾向はほぼ同じですが、レジスタ渡しへの変更による1.17での急激な高速化が確認できます。

tree-walkingインタプリタでのGoバージョンごとのGoAWKの実行速度

Goのregexpパッケージのパフォーマンスが改善されることを期待しています。現状はかなり遅いからです。正規表現はAWKスクリプトで多用されるため、実際のスクリプトでGoAWKを使う際には大きな違いが生まれるでしょう。いつか自分でも挑戦してみるかもしれません。

全体として、countwordsはGo 1.2でコンパイルした場合と比べて現在では約5倍、sumloopは約14倍高速になっています!(ただし、私がGoAWKを最初にリリースしたのはGoがすでに1.11のときだったので、初期の大きな高速化の恩恵を直接受けていたわけではありません。)

Goのように活発に開発が続いているコンパイラでは、ただ待っているだけで、他の人たちが大変な作業をやってくれてパフォーマンスが向上するのは素晴らしいことです。:-)

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント