Goのビルド時間の内訳について
Go1.14の古いlinkerについての記事です
$ go version
go version go1.14 darwin/amd64
リンカによるリンクはGoバイナリをビルドする際に最後に行われる工程です。ビルド時間の大半はリンクに費やされることが多く、ビルド時間の律速となりうる可能性が高いです。
実際にデバッグツールでビルドにかかる時間を分析してみましょう。
例として、Genjiを使ってデータベースを作成するというビルドに大幅な時間がかかるプログラムを用意しました。
後で詳細を述べますが、このパッケージはC言語のコードをGoでラップしたzstd
というパッケージに依存しています。
ビルドしてみると筆者の環境で大体3,4秒かかります。
"Hello world"のプログラムが1秒未満でできることを考えるとだいぶ時間がかかっていると言えるでしょう。このビルドで何が起きているかみていきましょう。
ビルド
Goプログラムのビルドはいくつかのコマンドをつなぎ合わせたものであり、-x
フラグをつけることで一連のコマンドログを出力させることができます。
下のコマンドログは、上のプログラムをビルドしてみた際のコマンドログを一部抜粋したものです。
ログは4段階に分かれています
- 最終生成物のバイナリを作るのに必要な中間ファイルを入れておく一時フォルダを作っています
- コンパイラは2つのファイルからアーカイブを作っています。
-pack
はアーカイブで出力させるためのフラグです - リンカが実行ファイルを作成しています。ここで使われているのは
clang
でデフォルトの外部リンカです。リンクの際に使うリンカはGoで実装された内部リンカとclang
やgcc
のような外部リンカのどちらかから選択できます。 - 生成されたバイナリはカレンとディレクトリに配置され一時フォルダは削除されます
ログだけではどこがビルド時間のボトルネックになっているかはわかりにくいので時間の内訳を可視化してみました。
コンパイルはキャッシュが効いて並列に実行されるので非常に高速な一方でリンクは非常に遅くビルド時間の大半を占めていることがわかります。
リンカについて
リンカの目標は実行ファイルを作成することです。
実行ファイルは、Goの標準ライブラリを含むパッケージに依存関係を持つコードを組み合わせたものです。
リンカの行う仕事は次のようになっています。
- コンパイルされたパッケージや依存ライブラリを読み込んで関数や変数のシンボルを集めます。
- 使われていない関数などのデットコードを取り除きます。これはコンパイル時にも行われますがコンパイルはパッケージ単位でコンパイルするので取り除かれないものもあります。リンカはパッケージ全体の情報を持っているので他のパッケージから使われていないデットコードを取り除くことができます。
-
DWARF
の生成。リンカは実行ファイルに含めるデバッグ情報を作成する役割を担います。 - PC-value tableや内部の関数テーブル、シンボルテーブルなどのメタデータの整理
- 再配置。パッケージ単位でコンパイルを行うのでコンパイラは他のパッケージの関数がどこにあるのかわかりません。リンカは全てのパッケージの情報を集めて各関数呼び出しに適切にアドレスを割り当てていく役割を担います。
次はリンクにかかる時間の内訳をみてみましょう。
ロード時間はパッケージの数によって変動します。どのパッケージがロードされたかは、-x
フラグを付けた時に出力されるldobj
をたどっていけばわかります。次に例を示します。
最後に行われるのが実行ファイルの生成です。これは今回Goでかかれた内部リンカでなく外部リンカを使用しているときのみ、実行されます。
このステップではビルドに必要なパッケージだけをリンクします。
内部リンカ
内部リンカは外部リンカより高速ですが、リンクに使用できない時もあります。
外部リンカを使う必要があるのは次のような場合です。
-
net
,os/user
,runtime/cgo
またはcgo
を使っている時 - プログラムがアーカイブやプラグイン、
pie
のようにexecutable
モードでビルドされなかった時 - ビルドターゲットが
android
,darwin/arm
,darwin/arm64
のとき
上の例では、依存パッケージがzstd
からC言語のコードをロードしているため外部リンカを使う必要があります。
幸運なことに、今回の場合は、環境変数CGO_ENABLED
を設定することで内部リンカを使ってビルドができます。
$ CGO_ENABLED=0 go build
1.48s user 0.64s system 154% cpu 1.374 total
ビルド時間が3倍ほど早くなりました。
今回のリンク時間の内訳です。
外部リンカを使っていた時はなかったものがあります。
DWARFセクションの圧縮です。DWARFセクションは外部リンカのときもありますが、圧縮は内部リンカを使う時のみ起こります。
実はDWARFセクションの圧縮の際に、ある程度再配置が行われているため、その後の再配置にかかる時間が外部リンカのときより短くなっています。
cgo
を無効にしたのでC言語で書かれたzstd
パッケージはもはや実行中に利用できません。しかしzstd
パッケージを使わないようにすればビルドは成功します。
zstd
パッケージを使っているGoプログラムをcgo
を無効にした状態でビルドしようとすると次のようなエラーが発生します。
$ go build
github.com/DataDog/zstd: build constraints exclude all Go files in /go/src/github.com/DataDog/zstd
また、-w
フラグをつければ、DWARFセクションを削除することができ、リンク時間をさらに短縮することができます。
デバッグ情報がいらない場合に使える最適化手法で、この場合のリンク時間の内訳は次のようになります。
最後に
リンカはGoツールチェーンで非常に重要な役割を担いますが、あまり積極的に改善がされてきませんでした。
Building a better Go linkerという文書でリンカに対する大幅な改善提案がなされ、実際に改良されたリンカが次のバージョン(1.15)で導入されることになっています。
Discussion