なぜC ++コンパイルはそんなに時間がかかるのですか?


540

C ++およびJavaと比較すると、C ++ファイルのコンパイルには非常に長い時間がかかります。C ++ファイルのコンパイルには、通常サイズのPythonスクリプトを実行するよりも大幅に時間がかかります。現在VC ++を使用していますが、どのコンパイラでも同じです。どうしてこれなの?

ヘッダーファイルを読み込んでプリプロセッサを実行することが2つの理由として考えられますが、それがなぜそれほど時間がかかるのかを説明できるとは思えません。


58
VC ++はプリコンパイル済みヘッダーをサポートしています。それらを使用すると役立ちます。たくさん。
ブライアン、

1
私の場合はい(ほとんどがいくつかのクラスを持つC-テンプレートなし)プリコンパイル済みヘッダーは約10倍高速化
Lothar

@ブライアン私はライブラリでコンパイル済みのヘッドを使用することはありませんでした
Cole Johnson

13
It takes significantly longer to compile a C++ file-1秒と比較して2秒という意味ですか?確かにそれは2倍の長さですが、ほとんど意味がありません。または、5秒ではなく10分を意味しますか?定量化してください。
Nick Gammon

2
私はモジュールに賭けました。C ++プロジェクトは、モジュールだけで行う他のプログラミング言語よりもビルドが速くなるとは思いませんが、ある程度の管理があれば、ほとんどのプロジェクトで非常に近くなります。モジュールの後にアーティファクトな統合を備えた優れたパッケージマネージャーが見られることを願っています
Abdurrahim、2018

回答:


800

いくつかの理由

ヘッダーファイル

コンパイルユニットごとに、数百または数千のヘッダーを(1)ロードして(2)コンパイルする必要があります。プリプロセッサは、ヘッダーのコンパイル結果がすべてのコンパイルユニット間で異なる可能性があることを保証するため、通常、それらすべてをコンパイルユニットごとに再コンパイルする必要あります。(マクロは、ヘッダーの内容を変更する1つのコンパイル単位で定義できます)。

これはおそらく、それがすべてのコンパイル単位のためにコンパイルされるコードの膨大な量を必要とし、さらに、すべてのヘッダは、(それを含むすべてのコンパイル単位に一度)を複数回コンパイルされなければならないように、主な理由。

リンク

コンパイルしたら、すべてのオブジェクトファイルをリンクする必要があります。これは基本的にモノリシックプロセスであり、あまり並列化できず、プロジェクト全体を処理する必要があります。

解析中

構文は解析が非常に複雑で、コンテキストに大きく依存し、明確にすることは非常に困難です。これには時間がかかります。

テンプレート

C#ではList<T>、プログラム内にリストのインスタンス化がいくつあるかに関係なく、コンパイルされるのはだけです。C ++では、vector<int>はから完全に分離したタイプvector<float>であり、それぞれを個別にコンパイルする必要があります。

これに加えて、テンプレートは、コンパイラが解釈する必要のある完全なチューリング完全な「サブ言語」を構成し、これは途方もなく複雑になる可能性があります。比較的単純なテンプレートメタプログラミングコードでも、数十および数十のテンプレートのインスタンス化を作成する再帰的なテンプレートを定義できます。テンプレートはまた、途方もなく長い名前を持つ非常に複雑な型をもたらす可能性があり、リンカーに多くの追加作業を追加します。(多くのシンボル名を比較する必要があり、これらの名前が何千もの文字に成長する可能性がある場合、それはかなり高価になる可能性があります)。

そしてもちろん、テンプレートは通常ヘッダーで定義する必要があるため、ヘッダーファイルの問題は悪化します。つまり、コンパイル単位ごとにはるかに多くのコードを解析してコンパイルする必要があります。通常のCコードでは、ヘッダーには通常、前方宣言のみが含まれますが、実際のコードはほとんど含まれません。C ++では、ほとんどすべてのコードがヘッダーファイルに存在することも珍しくありません。

最適化

C ++では、非常に劇的な最適化が可能です。C#またはJavaでは、クラスを完全に削除することはできません(リフレクションのためにクラスを削除する必要があります)が、単純なC ++テンプレートメタプログラムでも数十または数百のクラスを簡単に生成でき、それらはすべてインライン化され、最適化で再び削除されます。段階。

さらに、C ++プログラムはコンパイラーによって完全に最適化する必要があります。C#プログラムは、JITコンパイラに依存して、ロード時に追加の最適化を実行できます。C++では、このような「2番目のチャンス」はありません。コンパイラが生成するものは、取得するのと同じくらい最適化されています。

機械

C ++は、Javaまたは.NETのバイトコード(特にx86の場合)の使用よりも多少複雑になる可能性があるマシンコードにコンパイルされます。(これは、コメントなどで言及されたという理由だけで完全性から言及されています。実際には、このステップは、総コンパイル時間のごく一部以上を占めることはほとんどありません)。

結論

これらの要素のほとんどは、実際にかなり効率的にコンパイルされるCコードによって共有されます。C ++では解析ステップがはるかに複雑で、かなり時間がかかる可能性がありますが、主な違反者はおそらくテンプレートです。それらは有用であり、C ++をはるかに強力な言語にしますが、コンパイル速度の点でも犠牲になります。


38
ポイント3に関しては、CコンパイルはC ++よりも著しく高速です。コード生成ではなく、間違いなくスローダウンを引き起こすのはフロントエンドです。
トム

72
テンプレートについて:vector <int>はvector <double>とは別にコンパイルする必要があるだけでなく、vector <int>はそれを使用する各コンパイルユニットで再コンパイルされます。冗長な定義はリンカによって削除されます。
デビッドロドリゲス-ドリベス2008

15
dribeas:そうですが、それはテンプレートに固有のものではありません。インライン関数またはヘッダーで定義されているその他のものは、それが含まれるすべての場所で再コンパイルされます。しかし、そうです、それはテンプレートでは特に苦痛です。:)
jalf

15
@configurator:Visual Studioとgccはどちらもプリコンパイル済みヘッダーを使用できるため、コンパイルが大幅に高速化されます。
small_duck 2009年

5
DEBUGビルドは実際にはリリースモードビルドよりも遅いため、最適化が問題かどうかはわかりません。pdb世代も原因です。
gast128 2013

40

スローダウンはどのコンパイラでも同じではありません。

私はDelphiやKylixを使用していませんが、MS-DOS時代には、Turbo Pascalプログラムはほぼ瞬時にコンパイルされましたが、同等のTurbo C ++プログラムは単にクロールしていました。

2つの主な違いは、非常に強力なモジュールシステムと、シングルパスコンパイルを可能にする構文でした。

C ++コンパイラの開発者にとってコンパイル速度が優先事項ではなかった可能性は確かにありますが、C / C ++構文には、処理をより困難にする固有の複雑さもあります。(私はCの専門家ではありませんが、さまざまな商用C / C ++コンパイラーを構築した後、ウォルターブライトはD言語を作成しました。彼の変更の1つは、コンテキストフリーの文法を適用して、言語を解析しやすくすることでした。 。)

また、通常MakefileはすべてのファイルがCで個別にコンパイルされるように設定されているため、10個のソースファイルがすべて同じインクルードファイルを使用する場合、そのインクルードファイルは10回処理されます。


38
Pascalを比較するのは興味深いことです。NiklausWirthは、言語とコンパイラーを設計するときに、コンパイラーがそれ自体をベンチマークとしてコンパイルするのにかかった時間を使用したからです。高速シンボルルックアップ用のモジュールを慎重に作成した後、コードサイズの縮小によりコンパイラー自体のコンパイルが高速になったため、単純な線形検索に置き換えたという話があります。
ディートリッヒエップ

1
@DietrichEpp経験主義は報われます。
Tomas Zubiri

40

解析とコード生成は実際にはかなり高速です。本当の問題は、ファイルを開いたり閉じたりすることです。インクルードガードがあっても、コンパイラーは.Hファイルを開いて、各行を読み取ります(そして無視します)。

友人は(仕事で退屈している間)かつて彼の会社のアプリケーションを取り、すべて(すべてのソースファイルとヘッダーファイル)を1つの大きなファイルに入れました。コンパイル時間は3時間から7分に短縮されました。


14
まあ、ファイルアクセスは確かにこれに関係していますが、jalfが言ったように、これの主な理由は別の何か、つまり、あなたのケースでは完全に欠落する、非常に多くの(ネストされた!)ヘッダーファイルの繰り返し解析です。
Konrad Rudolph、

9
その時点で、友達はプリコンパイル済みヘッダーを設定し、異なるヘッダーファイル間の依存関係を解消し(あるヘッダーが別のヘッダーを含むのを避け、代わりに前方宣言する)、より高速なHDDを取得する必要があります。それはさておき、かなり驚くべき指標です。
トム・Leys

6
ヘッダーファイル全体(可能なコメントと空の行を除く)がヘッダーガード内にある場合、gccはファイルを記憶し、正しいシンボルが定義されている場合はスキップできます。
CesarB 2008年

11
解析は重要です。相互依存関係のある同じサイズのソース/ヘッダーファイルのNペアの場合、ヘッダーファイルを通過するO(N ^ 2)パスがあります。すべてのテキストを1つのファイルに入れると、重複した解析が削減されます。
トム

9
小さな注意事項:includeガードは、コンパイル単位ごとの複数の解析から保護します。全体的に複数の構文解析に反対するものではありません。
マルコファンデフォールト2012年

16

もう1つの理由は、宣言を見つけるためのCプリプロセッサの使用です。ヘッダーガードを使用した場合でも、.hはインクルードされるたびに何度も解析する必要があります。一部のコンパイラは、これに役立つプリコンパイル済みヘッダーをサポートしていますが、常に使用されるわけではありません。

参照:C ++のよくある質問の回答


回答のこの重要な部分を指摘するには、プリコンパイル済みヘッダーのコメントを太字にする必要があると思います。
ケビン

6
ヘッダーファイル全体(可能なコメントと空の行を除く)がヘッダーガード内にある場合、gccはファイルを記憶し、正しいシンボルが定義されている場合はスキップできます。
CesarB 2008年

5
@CesarB:コンパイル単位(.cppファイル)ごとに1回完全に処理する必要があります。
Sam Harwell

16

C ++はマシンコードにコンパイルされます。つまり、プリプロセッサ、コンパイラ、オプティマイザ、そして最後にアセンブラがあり、それらすべてを実行する必要があります。

JavaとC#はバイトコード/ ILにコンパイルされ、Java仮想マシン/.NET Frameworkは実行前に実行されます(またはJITはマシンコードにコンパイルされます)。

Pythonはインタプリタ言語であり、バイトコードにコンパイルされます。

これには他にも理由があると思いますが、一般的には、ネイティブマシン言語にコンパイルする必要がないため、時間を節約できます。


15
前処理によって追加されるコストはごくわずかです。スローダウンの主な「その他の理由」は、コンパイルが個別のタスク(オブジェクトファイルごとに1つ)に分割されるため、共通のヘッダーが繰り返し処理されるためです。それはO(N ^ 2)の最悪のケースですが、他のほとんどの言語のO(N)の解析時間と比較してです。
トム

12
同じ議論から、C、Pascalなどのコンパイラは遅いことがわかりますが、これは平均的には当てはまりません。それは、C ++の文法と、C ++コンパイラが維持しなければならない巨大な状態と、さらに関係があります。
セバスチャンマッハ2011年

2
Cは遅い。これは、受け入れられているソリューションと同じヘッダー解析の問題に悩まされています。たとえば、いくつかのコンパイルユニットにwindows.hを含む単純なWindows GUIプログラムを使用して、(短い)コンパイルユニットを追加する際のコンパイルパフォーマンスを測定します。
Marco van de Voort、2014

14

最大の問題は次のとおりです。

1)無限ヘッダーの再解析。すでに述べた。軽減策(#pragma onceなど)は通常、ビルドごとではなく、コンパイル単位ごとにのみ機能します。

2)ツールチェーンは多くの場合、複数のバイナリ(極端な場合はmake、プリプロセッサ、コンパイラ、アセンブラ、アーカイバ、impdef、リンカ、およびdlltool)に分離されているため、呼び出しのたびにすべての状態を常に再初期化および再ロードする必要があります(コンパイラ、アセンブラ)またはすべてのファイル(アーカイバ、リンカー、dlltool)。

またcomp.compilersの上でこの議論を参照してください:http://compilers.iecc.com/comparch/article/03-11-078特別に、この1:

http://compilers.iecc.com/comparch/article/02-07-128

comp.compilersのモデレーターであるJohnも同意しているようで、ツールチェーンを完全に統合してプリコンパイル済みヘッダーを実装すれば、Cでも同様の速度を実現できるはずです。多くの商用Cコンパイラはこれをある程度行っています。

すべてを個別のバイナリに分解するUnixモデルは、Windowsのワーストケースモデルの一種です(プロセスの作成が遅い)。Windowsと* nixの間でGCCビルド時間を比較する場合、特にmake / configureシステムが情報を取得するためだけにいくつかのプログラムを呼び出す場合、それは非常に顕著です。


13

C / C ++の構築:実際に何が起こり、なぜこれほど長い時間がかかるの

ソフトウェア開発時間の比較的大部分は、コードの記述、実行、デバッグ、さらには設計に費やされていませんが、コンパイルが完了するのを待っています。物事を速くするためには、まずC / C ++ソフトウェアがコンパイルされたときに何が起こっているのかを理解する必要があります。手順はおおよそ次のとおりです。

  • 構成
  • ビルドツールの起動
  • 依存関係チェック
  • コンパイル
  • リンク

次に、各ステップをより速くする方法に焦点を当てて、より詳細に見ていきます。

構成

これは、ビルドを開始するときの最初のステップです。通常、構成スクリプトまたはCMake、Gyp、SConsまたはその他のツールを実行することを意味します。Autotoolsベースの非常に大きなconfigureスクリプトの場合、これには1秒から数分かかることがあります。

このステップは比較的まれにしか発生しません。構成を変更するとき、またはビルド構成を変更するときにのみ実行する必要があります。ビルドシステムを変更することなく、このステップを高速化するために行うべきことは多くありません。

ビルドツールの起動

これは、makeを実行するか、IDE(通常はmakeのエイリアス)でビルドアイコンをクリックするとどうなるかです。ビルドツールバイナリが起動し、その構成ファイルとビルド構成を読み取ります。これらは通常、同じ構成です。

ビルドの複雑さとサイズに応じて、これには数分の1秒から数秒かかることがあります。これ自体はそれほど悪くはありません。残念ながら、ほとんどのmakeベースのビルドシステムでは、1回のビルドごとにmakeが数十回から数百回呼び出されます。通常、これはmakeの再帰的な使用が原因です(これは悪いことです)。

Makeが遅いのは実装上のバグではないことに注意してください。Makefileの構文には、非常に高速な実装を実現するためのいくつかの癖があります。この問題は、次のステップと組み合わせるとさらに顕著になります。

依存関係チェック

ビルドツールが構成を読み取ったら、変更されたファイルと再コンパイルが必要なファイルを判別する必要があります。構成ファイルには、ビルドの依存関係を記述する有向非循環グラフが含まれています。このグラフは通常、構成ステップ中に作成されます。ビルドツールの起動時間と依存関係スキャナーは、すべてのビルドで実行されます。それらを組み合わせたランタイムは、編集、コンパイル、デバッグのサイクルの下限を決定します。小規模なプロジェクトの場合、この時間は通常数秒程度です。これは許容範囲です。Makeに代わるものがあります。最速のものは忍者であり、これはGoogleのエンジニアがChromiumのために構築したものです。CMakeまたはGypを使用してビルドしている場合は、Ninjaバックエンドに切り替えるだけです。ビルドファイル自体を変更する必要はありません。速度を上げるだけです。忍者はほとんどのディストリビューションではパッケージ化されていませんが、

コンパイル

この時点で、最終的にコンパイラーを呼び出します。いくつかのコーナーをカットします。おおよその手順は次のとおりです。

  • マージに含まれるもの
  • コードを解析する
  • コード生成/最適化

一般的な考えに反して、C ++のコンパイルは実際にはそれほど遅くはありません。STLは低速で、C ++のコンパイルに使用されるほとんどのビルドツールは低速です。ただし、言語の遅い部分を軽減するためのより高速なツールと方法があります。

それらを使用するには、少しエルボグリースが必要ですが、その利点は否定できません。ビルド時間が短いほど、開発者はより幸せになり、俊敏性が向上し、最終的にはコードが改善されます。


9

コンパイルされた言語は常に、インタープリター型言語よりも大きな初期オーバーヘッドを必要とします。さらに、C ++コードを適切に構成していない可能性があります。例えば:

#include "BigClass.h"

class SmallClass
{
   BigClass m_bigClass;
}

コンパイルは次の場合よりかなり遅くなります。

class BigClass;

class SmallClass
{
   BigClass* m_bigClass;
}

3
BigClassが使用するファイルをさらに5つ含み、最終的にはプログラム内のすべてのコードを含める場合に特に当てはまります。
トム・レイズ

7
これはおそらく1つの理由です。しかし、たとえばPascalは、同等のC ++プログラムにかかるコンパイル時間の10分の1しかかかりません。これは、gcc:sの最適化に時間がかかるためではなく、Pascalの方が解析が簡単で、プリプロセッサを処理する必要がないためです。Digital Mars Dコンパイラーも参照してください。
ダニエルO

2
これは解析が容易ではありません。各コンパイル単位のwindows.hと他のヘッダーを再解釈することを回避するのはモジュール性です。はい、Pascalは簡単に解析できます(ただし、Delphiのような成熟したものは再び複雑になります)が、大きな違いはありません。
Marco van de Voort 2013年

1
ここに示す、コンパイル速度を向上させる手法は、フォワード宣言と呼ばれます。
DavidRR 2015

クラスを1つのファイルに書き込む。それは厄介なコードではないでしょうか?
Fennekin、2015年

8

大規模なC ++プロジェクトでコンパイル時間を短縮する簡単な方法は、プロジェクト内のすべてのcppファイルを含む* .cppインクルードファイルを作成し、それをコンパイルすることです。これにより、ヘッダーの爆発の問題が1回に減ります。これの利点は、コンパイルエラーが正しいファイルを参照することです。

たとえば、a.cpp、b.cpp、およびc.cppがあるとします。ファイルを作成します。everything.cpp:

#include "a.cpp"
#include "b.cpp"
#include "c.cpp"

次に、すべてを作成してプロジェクトをコンパイルします。cpp


3
私はこの方法への異議を認めません。スクリプトまたはMakefileからインクルードを生成すると仮定すると、それはメンテナンスの問題ではありません。実際、コンパイルの問題を難読化せずにコンパイルを高速化します。コンパイル時のメモリ消費を主張することもできますが、現代のマシンではほとんど問題になりません。それで、このアプローチの目的は何ですか(それが間違っているという主張は別として)?
rileyberton 2013年

9
@rileyberton(誰かがあなたのコメントに賛成していたため)を詳しく説明します。いいえ、コンパイルの速度は上がりません。実際、翻訳単位を分離しないことで、コンパイルに最大の時間がかかることを確認しています。それらについての素晴らしいところは、あなたがいること、あるいない、彼らは変更しなかった場合は、すべての.cpp-Sを再コンパイルする必要があります。(それは文体論を無視しています)。適切な依存関係管理と、おそらくプリコンパイル済みヘッダーの方がはるかに優れています。
2013年

7
申し訳ありませんが、これコンパイルを高速化するための非常に効率的な方法です。(1)リンクをほとんどなくし、(2)一般的に使用されるヘッダーを一度だけ処理すればよいためです。また、実際に試してみても問題ありません。残念ながら、それは増分再構築を不可能にするので、すべての構築は完全にゼロからです。しかし、この方法で再構築いっぱいであるたくさんより速く、あなたがそうでなければ取得したい何より
jalf

4
@BartekBanachewicz確かに、しかし、あなたが言ったことは、修飾子なしで「コンパイルをスピードアップしない」ということでした。おっしゃったように、すべてのコンパイルに最大の時間がかかります(部分的な再構築は行われません)が、同時に、それ以外の場合に比べて最大値が大幅に減少します。「これを行わない」よりも少しニュアンスがあると言っているだけです
jalf

2
静的変数と関数を楽しんでください。大きなコンパイルユニットが必要な場合は、大きな.cppファイルを作成します。
gnasher729 2014年

6

いくつかの理由は次のとおりです。

1)C ++文法はC#またはJavaよりも複雑で、解析に時間がかかります。

2)(より重要)C ++コンパイラはマシンコードを生成し、コンパイル中にすべての最適化を行います。C#とJavaは途中まで進んでおり、これらの手順はJITに任せています。


5

あなたが得ているトレードオフは、プログラムが少し速く実行されるということです。これは、開発中には冷たく感じるかもしれませんが、開発が完了し、プログラムがユーザーによって実行されているだけの場合は、かなりの問題になる可能性があります。


4

C ++ではコンパイル時に1回だけ実行されるアクションを実行するコストが原因でC#が常により遅く実行されることを言及することで、ほとんどの回答は少し不明確ですが、このパフォーマンスコストは実行時の依存関係にも影響されます実行する)、C#プログラムのメモリフットプリントが常に高くなることは言うまでもありません。その結果、パフォーマンスは使用可能なハードウェアの機能とより密接に関連します。同じことが、VMに依存または依存する他の言語にも当てはまります。


4

C ++でのプログラムのコンパイル速度に影響している可能性があると考えられる2つの問題があります。

考えられる問題#1-ヘッダーのコンパイル:これは、別の回答またはコメントですでに対処されている場合とされていない場合があります。)Microsoft Visual C ++(別名VC ++)は、プリコンパイル済みヘッダーをサポートしています。新しいプロジェクトを作成し、作成するプログラムのタイプを選択すると、セットアップウィザードウィンドウが画面に表示されます。下部にある[次へ>]ボタンをクリックすると、ウィンドウに機能のリストがいくつかあるページが表示されます。「プリコンパイル済みヘッダー」オプションの横のボックスがチェックされていることを確認してください。(注:これは、C ++のWin32コンソールアプリケーションでの私の経験ですが、C ++のすべての種類のプログラムに当てはまるわけではありません。)

考えられる問題#2-コンパイルされる場所:この夏、私はプログラミングコースを受講しました。使用していたラボのコンピューターが毎晩深夜に消去されたため、すべてのプロジェクトを8 GBフラッシュドライブに保存する必要がありました。それは私たちのすべての仕事を消してしまうでしょう。移植性/セキュリティなどのために外部ストレージデバイスにコンパイルする場合、非常に時間特に、かなり大きなプログラムの場合は、プログラムがコンパイルされるまでの時間(前述のプリコンパイル済みヘッダーを使用した場合でも)。この場合の私のアドバイスは、使用しているコンピューターのハードドライブ上でプログラムを作成およびコンパイルすることです。また、何らかの理由でプロジェクトの作業を停止する必要がある場合は、外部に転送することをお勧めしますストレージデバイスをクリックし、「ハードウェアの安全な取り外しとメディアの取り出し」アイコンをクリックします。これは、白いチェックマークが付いた小さな緑色の円の後ろに小さなフラッシュドライブとして表示され、接続を解除します。

これがお役に立てば幸いです。もしそうなら教えてください!:)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.