私は自分が設計しているプログラミング言語についてもっと考えていました。コンパイル時間を最小限に抑える方法は何ですか?
17 に答える
今日の主な問題はI/Oです。CPUはメインメモリよりも何倍も高速で、メモリはハードディスクにアクセスするよりも約1000倍高速です。
したがって、ソースコードを大幅に最適化しない限り、CPUはほとんどの時間をデータの読み取りまたは書き込みの待機に費やします。
次のルールを試してください。
いくつかの独立したステップで動作するようにコンパイラーを設計します。目標は、マルチコアCPUを利用できるように、各ステップを異なるスレッドで実行できるようにすることです。また、コンパイルプロセス全体を並列化するのにも役立ちます(つまり、同時に複数のファイルをコンパイルします)
また、多くのソースファイルを事前にロードして前処理できるため、実際のコンパイル手順をより高速に実行できます。
ファイルを個別にコンパイルできるようにしてください。たとえば、プロジェクトの「欠落しているシンボルプール」を作成します。シンボルが欠落していても、コンパイルエラーが発生することはありません。どこかに欠落しているシンボルが見つかった場合は、プールから削除します。すべてのファイルがコンパイルされたら、プールが空であることを確認します。
重要な情報を含むキャッシュを作成します。次に例を示します。ファイルXはファイルYのシンボルを使用します。これにより、Yが変更されたときに、ファイルZ(Y内の何も参照しない)のコンパイルをスキップできます。さらに一歩進めたい場合は、プール内のどこかに定義されているすべてのシンボルを配置します。シンボルが追加/削除されるようにファイルが変更された場合、影響を受けるファイルをすぐに知ることができます(ファイルを開かなくても)。
バックグラウンドでコンパイルします。プロジェクトディレクトリの変更をチェックするコンパイラプロセスを開始し、ユーザーがファイルを保存するとすぐにコンパイルします。このように、すべてではなく、毎回いくつかのファイルをコンパイルするだけで済みます。長期的には、コンパイルははるかに多くなりますが、ユーザーの場合、ターンオーバー時間ははるかに短くなります(=ユーザーが変更後にコンパイルされた結果を実行できるようになるまで待機する必要がある時間)。
「ジャストインタイム」コンパイラを使用します(つまり、インポートステートメントなどで使用されるときにファイルをコンパイルします)。その後、プロジェクトはソース形式で配布され、初めて実行されるときにコンパイルされます。Pythonはこれを行います。これを実行するために、コンパイラのインストール中にライブラリをプリコンパイルできます。
ヘッダーファイルは使用しないでください。すべての情報を1つの場所に保持し、必要に応じてソースからヘッダーファイルを生成します。たぶん、ヘッダーファイルをメモリに保存し、ディスクに保存しないでください。
コンパイル時間を最小限に抑える方法は何ですか?
- コンパイルなし(通訳言語)
- 遅延 (ジャスト イン タイム) コンパイル
- 増分コンパイル
- プリコンパイル済みヘッダー ファイル
私は自分でコンパイラを実装しましたが、人々が何百ものソースファイルをバッチフィードし始めたら、これを見なければならなくなりました。私は自分が知ったことにかなり驚いた。
最適化できる最も重要なことは文法ではないことがわかりました。字句解析器でもパーサーでもありません。代わりに、速度に関して最も重要なことは、ディスクからソース ファイルを読み取るコードです。ディスクへの I/O が遅い。本当に遅い。コンパイラの速度は、実行するディスク I/O の数でほぼ測定できます。
したがって、コンパイラを高速化するためにできる絶対的な最善の方法は、1 回の大きな I/O でファイル全体をメモリに読み込み、すべての字句解析、解析などを RAM から実行し、結果を書き出すことです。 1 つの大きな I/O でディスクに。
これについて、Gnat (GCC の Ada コンパイラ) を管理している責任者の 1 人に話を聞いたところ、彼は、ファイル I/O でさえ実際には RAM の読み取りと書き込みだけで済むように、実際にできることはすべて RAM ディスクに置いていたと言いました。
コンパイル速度とそれに影響を与えるものを測定することで学んだパフォーマンスの秘訣を次に示します。
文字から IR、IR からコードの 2 パス コンパイラを記述します。(文字 -> AST -> IR -> コードの3パス コンパイラを作成する方が簡単ですが、それほど高速ではありません。)
当然のことながら、オプティマイザはありません。高速なオプティマイザーを作成するのは困難です。
ネイティブ マシン コードの代わりにバイトコードを生成することを検討してください。Luaの仮想マシンは良いモデルです。
Fraser と Hanson がlccで使用したリニアスキャン レジスタ アロケータまたは単純なレジスタ アロケータを試してください。
単純なコンパイラでは、字句解析がパフォーマンスの最大のボトルネックになることがよくあります。C または C++ コードを作成している場合は、re2cを使用します。別の言語を使用している場合 (より快適に使用できます)、re2c に関する論文を読み、学んだ教訓を適用してください。
最大マンチまたは場合によっては iburgを使用してコードを生成します。
驚くべきことに、GNU アセンブラーは多くのコンパイラーでボトルネックになっています。バイナリを直接生成できる場合は、そうしてください。または、New Jersey Machine-Code Toolkit を確認してください。
上記のように、言語を設計して、 のようなものを避けてください
#include
。インターフェイス ファイルを使用しないか、インターフェイス ファイルをプリコンパイルしてください。この戦術により、多くの場合、最大のボトルネックであるレクサーの負担が大幅に軽減されます。
ほとんどの言語 (C++ 以外のすべての言語) では、個々のコンパイル ユニットのコンパイルは非常に高速です。
多くの場合、バインディング/リンクは遅いものです。リンカーは、単一のユニットではなく、プログラム全体を参照する必要があります。
C++ は、pImpl イディオムを使用しない限り、クライアント コードをコンパイルするために、すべてのオブジェクトとすべてのインライン関数の実装の詳細が必要になるという問題があります。
文法がオブジェクトとクラスを区別しないため、Java (ソースからバイトコードへ) が問題になります。Foo クラスをロードして、Foo.Bar.Baz が Foo クラスの Bar static フィールドによって参照されるオブジェクトの Baz フィールドであるかどうかを確認する必要がありますまたは Foo.Bar クラスの静的フィールド。2 つの間の Foo クラスのソースに変更を加えることができ、クライアント コードのソースは変更しませんが、クライアント コードを再コンパイルする必要があります。 . 私の知る限り、Python バイトコードは 2 つを区別しません。モジュールは、その親の真のメンバーです。
C++ と C は、プリプロセッサが各ヘッダーを何度も処理し、コンパイラがそれらをコンパイルする必要があるため、必要以上のヘッダーを含めると問題が発生します。ヘッダーのサイズと複雑さを最小限に抑えることが役立ち、モジュール性が向上するとコンパイル時間が短縮されることが示唆されます。ヘッダーが前処理されるときに存在する定義によって、そのセマンティクスや構文さえも変更される可能性があるため、ヘッダーのコンパイルを常にキャッシュできるとは限りません。
プリプロセッサを多用すると C は苦労しますが、実際のコンパイルは高速です。C コードの多くはtypedef struct _X* X_ptr
、C++ よりも実装を隠すために使用されます。C ヘッダーは、typedef と関数宣言で簡単に構成できるため、カプセル化が向上します。
そのため、クライアント コードから実装の詳細を隠す言語を作成することをお勧めします。また、インスタンス メンバーと名前空間の両方を持つ OO 言語の場合は、2 つにアクセスするための構文を明確にすることをお勧めします。真のモジュールを許可するため、クライアント コードは実装の詳細ではなくインターフェイスのみを認識する必要があります。プリプロセッサ マクロやその他のバリエーション メカニズムによって、参照されるモジュールのセマンティクスが変更されることを許可しないでください。
- 文法をシンプルで明確にすることで、すばやく簡単に解析できるようにします。
- ファイルのインクルードに強い制限を課します。
- 可能な限り完全な情報なしでコンパイルを許可します(たとえば、CおよびC ++での事前宣言)。
- 可能であれば、ワンパスコンパイル。
これがショットです..
ツールチェーンがサポートしている場合は、インクリメンタル コンパイルを使用します。(メイク、ビジュアルスタジオなど)。
たとえば、GCC/make では、コンパイルするファイルが多数ある場合に、1 つのファイルのみを変更すると、その 1 つのファイルのみがコンパイルされます。
Eiffel はさまざまな状態の凍結についての考えを持っており、再コンパイルは必ずしもクラス全体が再コンパイルされたことを意味しませんでした。
互換性のあるモジュールをどれだけ分割できますか? また、それらを追跡することにどれだけ関心がありますか?
これまでの回答で驚くほど欠けていることが 1 つあります。それは、文脈自由文法などを行うようにすることです。Pascal や Modula-2 など、Wirth によって設計された言語をよく調べてください。Pascal を再実装する必要はありませんが、文法設計は高速コンパイルのためにカスタムメイドされています。次に、アンダースがTurbo Pascalを実装するために引き出したトリックに関する古い記事を見つけられるかどうかを確認してください。ヒント: テーブル駆動。
コンパイラはどれほど深刻ですか?
構文がかなり複雑でない限り、パーサーは、入力ファイルの文字をインデックス付けするよりも10〜100倍遅く実行できるはずです。
同様に、コード生成は出力フォーマットによって制限する必要があります。
大量のヘッダーファイルを含むメガラインアプリを処理できる、大きくて深刻なコンパイラを実行している場合を除いて、パフォーマンスの問題が発生することはありません。
次に、プリコンパイル済みヘッダー、最適化パス、およびリンクについて心配する必要があります。
コンパイル時間を最小限に抑えるために行われた作業はあまり見たことがありません。しかし、いくつかのアイデアが思い浮かびます:
- 文法は単純にしてください。複雑な文法は、コンパイル時間を増やします。
- マルチコア GPU または CPU を使用して、並列処理を利用してみてください。
- 最新のコンパイラをベンチマークし、ボトルネックとは何か、およびそれらを回避するためにコンパイラ/言語で何ができるかを確認してください。
高度に専門化された言語を作成している場合を除き、コンパイル時間は実際には問題になりません..
悪くないビルドシステムを作ろう!
コンパイルに 1 秒もかからないソース ファイルがおそらく 3 つある膨大な量のプログラムがありますが、そこまでたどり着く前に、int
. そして、その 1 分後に別のものをコンパイルしようとすると、ほぼ同じ一連のテストを行う必要があります。
したがって、コンパイラがs のサイズを変更したり、実行間で基本的な関数の実装を変更したりするなど、ユーザーにひどいことをしていない限り、int
その情報をファイルにダンプして、2 分ではなく 1 秒で取得できるようにします。
- 最初にコンパイルしようとしたときに、すべてがコンパイルできることを確認してください。たとえば、前方参照を禁止します。
- 文脈自由文法を使用して、記号テーブルなしで正しい解析ツリーを見つけられるようにします。
- 構文からセマンティクスを推測できることを確認してください。これにより、解析ツリーとシンボル テーブルをいじるのではなく、正しい AST を直接構築できます。
簡単なもの: コンパイラがマルチコア CPU をネイティブに利用できることを確認します。
プログラミングしている言語/プラットフォームによって異なります。.NET 開発の場合は、ソリューションに含めるプロジェクトの数を最小限に抑えてください。
昔は、RAM ドライブをセットアップしてそこでコンパイルすることで、劇的なスピードアップを得ることができました。ただし、これがまだ当てはまるかどうかはわかりません。
C++ では、 Incredibuildなどのツールで分散コンパイルを使用できます