Zigのインクリメンタルコンパイルの仕組みを徹底解剖!爆速ビルドの裏側とは?
Zig's Incremental Compilation Internals
Zig's Incremental Compilation Internals
Zig言語の最大の特徴である、驚異的に高速なインクリメンタルコンパイルの内部構造について解説します。Zigがどのように依存関係を追跡し、最小限の再コンパイルで変更を反映させているのか、その舞台裏に迫ります。
これってリリースビルドでも使えるの?それともデバッグビルドだけ?
ZigコンパイラはCもコンパイルできるけど、それならCでも動くってこと?
業界が長年コンパイル速度を軽視してきたことに失望してたんだよね。Zigがここで非常に価値があってインパクトのある仕事をしているのを見るのは嬉しいよ。
Zigのツールチェーン周りの取り組みは相変わらず素晴らしいね。メモリ安全性が必須条件だと考えている自分はZigでソフトウェアを書く予定はないけれど、こうした進歩はとても素晴らしいと思う。インクリメンタル(増分)コンパイルの取り組み以前の、クロスコンパイラとしてのツールチェーンの完成度もずっと凄かったし。次は何が出てくるのかすごく楽しみ!
セマンティック解析はコンパイラの中でインクリメンタルに扱うのが最も難しい部分だ。だからこそ驚くことではないかもしれないが、ここで言語設計が大きく関わってくる。ほとんどのモダンな言語で、自分たちがやっているようなインクリメンタルコンパイルはサポート可能だと思うけれど、設計上の決定次第でそれは非常に難しくなる。Zigは長年にわたって、高速なインクリメンタルコンパイルをサポートしやすくするために(時として物議を醸しつつも)設計を調整してきたんだ。
これ、Rustでもやっておけばよかったと今になって思うよ。とはいえ、一度にすべてを実現するのは不可能だし、当時はすでに山のようなタスクがあった。これは「いつ1.0をリリースするか」というトレードオフの一部でもあるんだ。言語の目標からすると2015年のリリースは正しいタイミングだったけれど、熟成させる期間があと数年あれば、コンパイル速度をもっと高速化できていたかもしれない。ソフトウェアエンジニアリングってのは本当に難しいよ。
この設計で一つよくわからないことがある。なぜデバッグビルドで全てのコードを含む巨大なバイナリを作ることにこだわるんだろう?自分の考えでは、もっと単純にファイル単位などで小さな共有ライブラリをたくさん生成して、それを最終的なバイナリにリンクさせる方がいいと思う。そうすればプログラムのバイナリ本体はテキストセクションが極小になり、あとはロードすべき共有ライブラリのリストが(長くはなるだろうけど)あるだけになる。これなら何千もの共有ライブラリを読み込むことになっても、プログラム本体は大きくならないし、バイナリパッチも必要ないはずだ。
リリースモードなら巨大なバイナリが好ましいのはわかるけれど、デバッグビルドでこの設計を採用する理由が理解できないんだ。それに、記事を読んでいて疑問に思ったのが、メインのバイナリが壊れたらどうするのかってこと。例えばバイナリをパッチしている最中にユーザーがCtrl+Cでコンパイルを中断したりしたらどうなるんだろう?破損を防いだり検知したりする仕組みがあったとしても、最初からパッチなんてせずに常に新しいメインバイナリを生成する方が単純じゃないかな。新しいバイナリといっても、リンクする共有ライブラリのリストが主だから容量は食わないし、ディスクにもすぐに書き込めるはずだしね。さらにこのプロセスを再帰的(例えばサブディレクトリ単位)に行えば、インクリメンタルなリンク時に新しいコードをパッチして再配置を連鎖させるようなことをせずとも、小さな共有ライブラリをいくつか作るだけで済むと思うんだけど。
ずっと面白いテーマだと思ってきたけど、自分が知っているインクリメンタルコンパイルといえばマイナーな言語かRustくらいだったな。ああ、Roslynとかもあったっけ?
取り組むには本当に楽しくて魅力的な問題だね。
ランタイム関数のボディへの依存関係を追跡するのは不可能(少なくともここで示している単純化した見方では)
定数がcomptime関数によって計算される場合があるけれど、その場合はどうなるの?
この記事は非常に面白いね。rust-analyzerチームの一員として、Rust界隈の状況と比較せずにはいられない。Rustは(有名な話だけど)インクリメンタルコンパイルのための高度なシステムを持っているはずなのに、コンパイル速度は圧倒的に遅い。原因は主に2つあると考えている。
言語設計。Zigは高速なインクリメンタルコンパイルを前提に設計されているが、Rustはそうではない。例えば、この記事ではコンパイラが変更を追跡すべき4つのプロパティ(レイアウト、型、値、ボディ)について述べている。Rustはこれよりも遥かに多くのプロパティを持っていて、静的に追跡するのは不可能に近い。その結果、コンパイラは動的に追跡するクエリシステムを使うことになり、それがオーバーヘッドになっているんだ。
コンパイラの実装。RustはZigよりもコンパイルが遥かに複雑だ。rustcはZigのコンパイラよりも古く、コード量も10倍から20倍大きいから、変更を加えるのが格段に難しくなっている。
試しに触ってみようと zig cc を知ってから、すっかりZigのファンになってしまったよ。ビルドキャッシュの時点で感動していたから、これも試してみるのが楽しみだ。