プログラミング
コンパイラ、ビルドシステム、そしてその先の間のコミュニケーション
Communication Between the Compiler, the Build System, and Beyond (shrub.industries)
要約
この記事は、コンパイラ、ビルドシステム、パッケージマネージャーが生成する依存関係やリソース情報をシステム全体で共有しないことによる、オペレーティングシステムにおける最適化の機会損失という問題提起を行っています。コンパイラとビルドシステムの緊密な連携や、ビルドシステムとパッケージマネージャー間の依存関係知識の共有が、プログラム最適化、ディスク容量削減、開発効率向上、システム管理の透明性向上など、多岐にわたる利点をもたらす可能性について論じています。
全文翻訳
problem(1) General Commands Manual problem(1) 問題の規模はどれくらいか?現代のオペレーティングシステム(主にPOSIXライクなFOSSのもの、これらが最も一般的で私が使用しているものであるという理由で)には問題があります。この問題の適切な言葉が何であるかは分かりませんが、ユーザーに問題を引き起こすような問題ではなく、むしろ機会損失を引き起こす問題です。問題とは、コンパイラ、ビルドシステム、パッケージマネージャーが提供する有用な依存関係およびリソース情報が互いに、あるいはシステムの他の部分と共有されないため、オペレーティングシステムが多くの種類の有用な最適化の可能性を逃しているということです。私が当初、問題がどこから始まりどこで終わると考えていたのか、そしてそれが当初考えていたよりもはるかに広範囲に及んだのか、そして実際のプログラム最適化、ディスク容量の節約、ソフトウェア開発の容易化、透明なシステム管理、より良いスケジューリングなど、あらゆる種類の興味深い最適化の機会の具体的な例をいくつか説明します。これはシステムコンポーネント間のコミュニケーションを可能にすることで実現されます。
コンパイラとビルドシステム
私がこの問題に最初に気づいたのは、現代の言語のビルドシステムと、ほとんどのオペレーティングシステムのずっと古い基盤であるCのビルドシステムを比較していたときでした。現代の言語(Rust、Go、Zig)では、コンパイラとビルドシステムは緊密に統合されています。これにより、言語ユーザーはビルドシステムを選択する必要がなくなり、その言語を使用するすべてのプロジェクトでソフトウェアをビルドするための標準化された方法が得られ、他の多くの利点が得られます(次のセクションでさらに詳しく説明します)。しかし、これは現代のほとんどの言語が全く行っていない、他の多くの利点も提供します。これらの利点を説明するために、Cコンパイラと緊密に統合されたCの仮説的なビルドシステムの例を使用します。現在存在するすべてのCのビルドシステムでは、各コンパイル単位(.cファイル)は、ビルドシステムによって起動された個別のプロセスのもとで、互いに完全に独立して出力(.oファイル)にコンパイルされます。コンパイラにとって、これはプログラムに関する唯一のコンテキストが目の前にある単一のコンパイル単位であるため、実行できる最適化のレベルは、その単一のコンパイル単位内で完全に完結するものです。これが問題となる簡単な例を挙げます:
/*four.c*/
int xfour(int x) {
return x * 4;
}
/*main.c*/
extern int xfour(int);
int main(void) {
return xfour(3);
}
この例では、2つのファイルがあります。コンパイラは、互いの知識なしに各ファイルを個別にコンパイルします。コンパイラは、4倍の乗算が何かを4倍するための最良の可能なアセンブリ命令を使用するように最善を尽くしますが、呼び出し元が3を渡すことを全く知りません。しかし、もし両方のファイルに関する知識があれば、呼び出し元がxfourに何を渡すかを知ることができ、mainをはるかに単純なものに最適化できます:
(明確さのためにCでの例ですが、コンパイラはアセンブリを出力します)
int main(void) {
return 12;
}
Cコンパイラやコンパイル全般について何か知っていれば、これはプログラム全体の最適化(whole program optimisation)と呼ばれ、完全に可能であり、かなり一般的であることを知っているでしょう。これはLTO、リンク時最適化として知られています。しかし、私が思うに、リンク時に行われるその方法は、プログラム全体の最適化を実行するのに最適な時期ではありません。LTOの仕組みは次のとおりです:コンパイラは各.cコンパイル単位をコンパイラIRを含む.oファイルに個別にコンパイルし、各コンパイル単位で最善の最適化を行います。次に、リンカはIRを含むすべての.oファイルを受け取り、それらすべてを使用してプログラム全体の最適化を行い、それらを単一の成果物(実行可能ファイルまたはライブラリ)にリンクします。
しかし、1つに統合されたビルドシステムとコンパイラを想像してみてください。これにより、ビルドシステムは、リンク時まで待つ必要なしに、各コンパイル単位がどこに配置されるかを知ることができます。次のようなビルド記述を想像してください:
exe four {
sources: [ “four.c” , “main.c” ]
}
このような記述があれば、ビルドシステムはfour.cとmain.cの両方がfourという1つの出力になることを知っています。ビルドシステムはコンパイラにこれを伝えることができ、コンパイラはオブジェクトがリンクされる前にプログラム全体の最適化を行うことができます。当初、これはLTOよりもはるかに高速になると考えていましたが、実際には同じくらいになると思います(LTOの作業のほとんどは最適化なので、いつ行うかはあまり関係ありません)。実装レベルではLTOよりもわずかに高速になる可能性があると考えています(.oファイルとしてIRをディスクに書き込むのを避けます)。しかし、ビルドシステムがコンパイラと通信できることから、他の利点もあります。たとえば、複数の.cファイルが同じヘッダーを#includeしている場合、コンパイルプロセスが互いに独立していないため、同じヘッダーの解析とASTの構築を繰り返し行うことを避け、コードが重複している場合(これはやや可能性が低いですが)は、IRとASTをファイル間でより自由にキャッシュできます。さらに、プログラム全体に関する知識があれば、通常Cで行われるよりもはるかに小規模なレベルで増分コンパイルを実行するのに十分な情報が得られます。一般的に、増分コンパイルは各コンパイル単位のタイムスタンプに基づいて行われます。しかし、ライブラリ/実行可能ファイルの各出力を作成するすべてのコンパイル単位を、ビルドシステムのビルドグラフから知っている1つのコンパイル単位として扱う場合、Rustがすでに要求ベースのコンパイルシステムで行っているような、関数レベルでの増分コンパイルを行うことができます。
これを考えた後、さらに多くの潜在的な利点を得るために、この種のシステムをビルドを意識したパッケージマネージャーと組み合わせる方法がすぐに思い浮かびました。それをこれから示します。
コンパイラ、ビルドシステム、そしてパッケージマネージャー
ほとんどの現代の言語には、開発者がソフトウェアの依存関係をある程度自動化された方法で簡単に管理できるようにするためのパッケージ管理システムが含まれています。これにより、ソフトウェアをコンパイルしたいユーザーは、あなたのソフトウェアを取得するために、任意の数の依存関係を手動でコンパイルする必要がなくなります。Cには実際にはこれといったものはありません。ただし、ある意味ではあります。依存関係を持つほとんどのCプログラムは、必要なライブラリとヘッダーを提供できるシステムパッケージマネージャー(apt、apk、pacman、dnf、xbps、pkginなどを考えてください)を期待しています。もちろん、これはGoのような言語が行っていることとは異なります。Goは依存関係のソースを取得し、自動的にコンパイルしてから、その新しくコンパイルされたコピーを使用します。現在のシステムは、リモートリポジトリから事前コンパイルされたパッケージが利用可能であることを期待しています。私はこれが理想的ではないと思います。1つには、ソフトウェアをコンパイルするとき、ユーザーはリモートリポジトリからこれらのバイナリパッケージを取得できることを期待しますが、これは自分で作成していない任意のバイナリ成果物を信頼することを含みます。しかし、より重要なことに、プログラム全体の最適化から最大の最適化ポテンシャルを得るためには、リンクするライブラリを含む、プログラム全体を最適化する必要があります。これは技術的にはリンク時最適化のみを使用して可能です。リンクするライブラリは、IRを含むオブジェクトの静的アーカイブである必要があります。これにはいくつかの問題があります。最初の問題は、異なるコンパイラによって生成されたIRは互換性がないことです。2番目の問題は、ほとんどのシステムパッケージマネージャーによって配布されている(もし配布されている場合)静的ライブラリにはIRがなく、通常のオブジェクトファイルであるため、それらとリンクする場合にLTOを実行できないことです。したがって、より良い解決策は、パッケージがそれ自体のビルド方法と、それらの依存関係およびそれらに依存するものがどのようにビルドされるかを知っているパッケージマネージャーです。私はこれをパッケージ境界を越えた依存関係知識と呼びます。これがどのように有益であるかの例をいくつか示します。前の乗算の例を想像してみてください。ただし、four.cは今や