[インデックス 17353] ファイルの概要
このコミットは、Goコンパイラのバックエンドにおける最適化に関するものです。具体的には、cmd/5g
(ARM), cmd/6g
(x86-64), cmd/8g
(x86) の各アーキテクチャ向けコンパイラにおいて、コード生成後の最適化フェーズ(peephole optimizer)で利用されるデータ構造の初期化方法を変更することで、コンパイル速度の向上を図っています。従来のリンクリストを辿る初期化処理をmemset
による一括初期化に置き換えることで、go install -a std
コマンドの実行時間を約10%削減することに成功しています。
コミット
cmd/5g, cmd/6g, cmd/8g: faster compilation
Replace linked list walk with memset.
This reduces CPU time taken by 'go install -a std' by ~10%.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/06e686def67297a08ecb75e3543fde1284c1ee61
元コミット内容
commit 06e686def67297a08ecb75e3543fde1284c1ee61
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Aug 21 14:20:28 2013 +0400
cmd/5g, cmd/6g, cmd/8g: faster compilation
Replace linked list walk with memset.
This reduces CPU time taken by 'go install -a std' by ~10%.
Before:
real user sys
0m23.561s 0m16.625s 0m5.848s
0m23.766s 0m16.624s 0m5.846s
0m23.742s 0m16.621s 0m5.868s
after:
0m22.714s 0m14.858s 0m6.138s
0m22.644s 0m14.875s 0m6.120s
0m22.604s 0m14.854s 0m6.081s
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/13084043
変更の背景
Goコンパイラは、ソースコードを機械語に変換する過程で様々な最適化を行います。その中でも、peephole optimizer(ピーフホール最適化)は、生成された機械語コードの小さな連続した部分(「ピーフホール」と呼ばれる)を検査し、より効率的な命令シーケンスに置き換えることで、コードの品質と実行速度を向上させる役割を担っています。
このコミットの背景には、コンパイル時間の最適化という明確な目標がありました。Go言語は高速なコンパイルを特徴の一つとしていますが、大規模なプロジェクトや標準ライブラリ全体をコンパイルする際には、わずかな改善でも全体的な開発体験に大きな影響を与えます。
コミットメッセージに示されているように、go install -a std
コマンド(標準ライブラリ全体を再コンパイルしてインストールするコマンド)の実行時間が約10%削減されたことは、この最適化が非常に効果的であったことを示しています。これは、コンパイラのボトルネックとなっていた部分を特定し、より効率的なアルゴリズムに置き換えることで達成されました。具体的には、peephole optimizer内で使用されるactive
フラグの初期化処理が、リンクリストの走査からmemset
によるメモリ一括クリアに変更されたことが、この性能向上に寄与しています。
前提知識の解説
Goコンパイラ (cmd/5g
, cmd/6g
, cmd/8g
)
Goコンパイラは、Go言語のソースコードを各アーキテクチャの機械語に変換するツールです。cmd/5g
はARMアーキテクチャ、cmd/6g
はx86-64アーキテクチャ、cmd/8g
はx86アーキテクチャ向けのコンパイラを指します。これらはGoの初期のコンパイラであり、C言語で記述されていました。
Peephole Optimizer (ピーフホール最適化)
peephole optimizerは、コンパイラのバックエンドで行われる最適化手法の一つです。コード生成の最終段階で、生成された機械語命令列を小さな「窓(peephole)」を通して見て、非効率な命令シーケンスをより効率的なものに置き換えます。例えば、冗長なロード/ストア命令の削除、不要なジャンプ命令の削除、より短い命令への置き換えなどを行います。
この最適化は、通常、命令の局所的なパターンに焦点を当てるため、コンパイラの他の最適化フェーズ(例えば、レジスタ割り当てやループ最適化など)とは独立して適用されることが多いです。
Flow
構造体とactive
フラグ
Goコンパイラのpeephole optimizerでは、プログラムの制御フローグラフ(Control Flow Graph, CFG)を表現するためにFlow
のようなデータ構造が使用されます。Flow
構造体は、プログラムの各命令(Prog
)間の関係や、最適化の過程で必要となる様々な情報を保持します。
active
フラグは、peephole optimizerが特定の最適化(例えば、冗長なコピーの伝播 copyprop
)を行う際に、既に処理済みであるか、または現在処理中であるかをマークするために使用されるフラグです。これにより、無限ループを防いだり、重複した処理を避けたりします。最適化の各イテレーションの開始時には、このactive
フラグをリセット(通常は0に設定)する必要があります。
リンクリストの走査とmemset
- リンクリストの走査 (Linked list walk): 従来の
active
フラグのリセット方法では、Flow
構造体のリンクリストを先頭から順に辿り、各Flow
要素のactive
フィールドを個別に0に設定していました。これは、リストの要素数に比例して時間がかかる処理です。 memset
:memset
はC言語の標準ライブラリ関数で、指定されたメモリ領域を特定の値で埋めるために使用されます。このコミットでは、active
フラグを保持する配列(uchar *active
)を導入し、その配列全体をmemset
を使って一括で0に初期化するように変更されました。memset
は通常、非常に高速に動作し、特に大きなメモリブロックを初期化する場合には、個々の要素をループで設定するよりもはるかに効率的です。
技術的詳細
このコミットの核心は、peephole optimizerのcopyprop
関数内で使用されるactive
フラグの初期化方法の変更です。
従来のcopyprop
関数では、Flow
構造体のリンクリストをfor(r=g->start; r!=nil; r=r->link)
のように走査し、各r->active = 0;
と設定していました。これは、Flow
構造体の数が多くなると、その走査と個別の書き込みに時間がかかるボトルネックとなっていました。
新しいアプローチでは、active
フラグをFlow
構造体から分離し、uchar *active
という独立した配列として管理するように変更されました。この配列のサイズは、制御フローグラフのノード数(g->num
)に基づいて動的に確保されます(realloc
を使用)。そして、このactive
配列の初期化には、memset(active, 0, g->num);
が使用されます。
さらに、Flow
構造体にはactive
フィールドが残されていますが、これはactive
配列のインデックスとして利用されるようになりました。具体的には、peep
関数の冒頭で、リンクリストを一度走査し、各Flow
要素のr->active
にその要素のインデックス(t
)を割り当てています。これにより、copy1
関数内でr->active
を参照する際に、active[r->active]
という形でactive
配列の対応するフラグにアクセスできるようになります。
この変更により、copyprop
が呼び出されるたびに、active
フラグのリセットがリンクリストの走査ではなく、高速なmemset
によって行われるようになり、コンパイル時間の短縮に貢献しました。
コアとなるコードの変更箇所
変更はsrc/cmd/5g/peep.c
, src/cmd/6g/peep.c
, src/cmd/8g/peep.c
の3つのファイルに共通して適用されています。
peep
関数内での変更
// Before: (no explicit initialization of r->active here)
// After:
+ for(r=g->start, t=0; r!=nil; r=r->link, t++)
+ r->active = t;
peep
関数の冒頭で、Flow
リンクリストを一度走査し、各Flow
要素のactive
フィールドに、その要素のリスト内でのインデックスを割り当てています。これにより、Flow
構造体のactive
フィールドが、active
配列へのオフセットとして機能するようになります。
グローバル変数/静的変数の追加
+static uchar *active;
+static int nactive;
active
フラグを保持するためのuchar
型ポインタactive
と、その配列の現在のサイズを保持するnactive
が追加されました。これらはファイルスコープの静的変数として宣言されています。
copyprop
関数内での変更
// Before:
// for(r=g->start; r!=nil; r=r->link)
// r->active = 0;
// After:
+ if(nactive < g->num) {
+ nactive = g->num;
+ active = realloc(active, g->num);
+ }
+ memset(active, 0, g->num);
copyprop
関数内で、active
配列のサイズが現在の制御フローグラフのノード数(g->num
)よりも小さい場合、realloc
を使って配列を拡張します。その後、memset
を使ってactive
配列全体を0で初期化します。これにより、リンクリストの走査が不要になりました。
copy1
関数内での変更
// Before:
// if(r->active) {
// After:
+ if(active[r->active]) {
// Before:
// r->active = 1;
// After:
+ active[r->active] = 1;
copy1
関数内でactive
フラグを参照・設定する箇所が、r->active
からactive[r->active]
に変更されました。これは、r->active
がもはやブール値のフラグではなく、active
配列へのインデックスとして機能するためです。
コアとなるコードの解説
このコミットの主要な変更点は、copyprop
関数におけるactive
フラグの初期化ロジックです。
-
Flow
構造体へのインデックス割り当て:peep
関数の冒頭で、一度だけFlow
リンクリストを走査し、各Flow
ノードに一意のインデックスを割り当て、それをr->active
に格納します。このr->active
は、もはやブール値のフラグではなく、active
配列へのオフセットとして機能します。この処理は、peephole optimizerの開始時に一度だけ行われます。 -
active
配列の動的確保とmemset
による初期化:copyprop
関数が呼び出されるたびに、まずactive
配列の現在のサイズ(nactive
)が、現在の制御フローグラフのノード数(g->num
)よりも小さいかどうかをチェックします。if(nactive < g->num)
: もしactive
配列が小さすぎる場合、realloc
を使ってg->num
バイトのメモリを確保し、nactive
を更新します。これにより、active
配列は常に現在のグラフのノード数をカバーできる十分なサイズを持つことが保証されます。memset(active, 0, g->num);
: その後、memset
関数を使って、active
配列の先頭からg->num
バイト分をすべて0で埋めます。これにより、すべてのactive
フラグが効率的にリセットされます。memset
は通常、アセンブリ言語で最適化されており、CPUのキャッシュラインを効率的に利用するため、リンクリストを個別に走査するよりもはるかに高速です。 -
active
フラグへのアクセス方法の変更:copy1
関数内では、active
フラグのチェックと設定がif(r->active)
からif(active[r->active])
へ、そしてr->active = 1;
からactive[r->active] = 1;
へと変更されました。これにより、Flow
構造体内のactive
フィールドが指すインデックスを使って、グローバルなactive
配列の対応する要素にアクセスするようになります。
この一連の変更により、peephole optimizerの重要な部分であるactive
フラグのリセット処理が大幅に高速化され、結果としてGoコンパイラ全体のコンパイル時間が短縮されました。これは、データ構造の選択とアルゴリズムの最適化が、ソフトウェアの性能に大きな影響を与える良い例と言えます。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Goのコンパイラに関するドキュメント (古い情報も含む): https://golang.org/doc/install/source
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritチェンジリスト: https://golang.org/cl/13084043
参考にした情報源リンク
memset
に関するC言語のドキュメント: https://www.cplusplus.com/reference/cstring/memset/- コンパイラ最適化に関する一般的な情報源 (例: Dragon Book - Compilers: Principles, Techniques, and Tools)
- Go言語のコンパイラ設計に関する議論やドキュメント (Goの公式ブログやデザインドキュメントなど)
- Goの初期のコンパイラ(
cmd/5g
,6g
,8g
)のソースコード - 制御フローグラフ (CFG) に関する情報
- Peephole Optimizationに関する情報
realloc
に関するC言語のドキュメント: https://www.cplusplus.com/reference/cstdlib/realloc/- Goのコンパイラ開発者コミュニティの議論 (golang-devメーリングリストなど)
- GoのIssue Tracker (Goのバグや機能リクエストが管理されている場所)
- Goのソースコード内のコメントやREADMEファイル
- Goの歴史的なコミットログ