C ++コンパイル時間を短縮するために使用できるテクニックは何ですか?
この質問は、Stack Overflowの質問C ++プログラミングスタイルへのコメントで出てきました。どんなアイデアがあるのか知りたいです。
私は関連する質問を見ました、なぜC ++コンパイルはそんなに時間がかかるのですか?、しかしそれは多くの解決策を提供しません。
C ++コンパイル時間を短縮するために使用できるテクニックは何ですか?
この質問は、Stack Overflowの質問C ++プログラミングスタイルへのコメントで出てきました。どんなアイデアがあるのか知りたいです。
私は関連する質問を見ました、なぜC ++コンパイルはそんなに時間がかかるのですか?、しかしそれは多くの解決策を提供しません。
回答:
こことここで、不透明なポインターまたはハンドルクラスとも呼ばれるPimplイディオムを 見てください。コンパイルを高速化するだけでなく、スローしないスワップ関数と組み合わせると例外の安全性も向上します。Pimplイディオムを使用すると、ヘッダー間の依存関係を減らし、実行する必要がある再コンパイルの量を減らすことができます。
可能な限り、前方宣言を使用してください。コンパイラーがそれSomeIdentifier
が構造体またはポインターなどであることだけを知る必要がある場合は、定義全体を含めずに、コンパイラーに必要以上の処理を強います。これにはカスケード効果があり、必要な速度よりも遅くなります。
I / Oの流れは特にビルドを減速のために知られています。ヘッダーファイルで必要な場合は、<iosfwd>
代わりに<iostream>
#includeを試し、#includeを<iostream>
実装ファイルのみに含めます。<iosfwd>
ヘッダーには、前方宣言のみを保持しています。残念ながら、他の標準ヘッダーにはそれぞれの宣言ヘッダーがありません。
関数シグネチャでは、値渡しよりも参照渡しを優先します。これにより、ヘッダーファイルにそれぞれの型定義を#includeする必要がなくなり、型を前方宣言するだけで済みます。もちろん、不明瞭なバグを避けるために、const参照を非const参照よりも優先しますが、これは別の質問の問題です。
ガード条件を使用して、ヘッダーファイルが1つの変換単位に複数回含まれないようにします。
#pragma once
#ifndef filename_h
#define filename_h
// Header declarations / definitions
#endif
プラグマとifndefの両方を使用することにより、プレーンマクロソリューションの移植性と、一部のコンパイラーがpragma once
ディレクティブの存在下で実行できるコンパイル速度の最適化が得られます。
一般に、コード設計がモジュール化され、相互依存性が低いほど、すべてを再コンパイルする必要が少なくなります。また、追跡する必要が少ないという事実により、コンパイラが個々のブロックに対して同時に実行する必要がある作業量を減らすことにもなります。
これらは、含まれるヘッダーの共通セクションを一度に多くの翻訳単位用にコンパイルするために使用されます。コンパイラはそれを1回コンパイルし、その内部状態を保存します。次に、その状態をすばやくロードして、同じヘッダーのセットを持つ別のファイルをコンパイルする際に有利なスタートを切ることができます。
まれに変更されるものだけをプリコンパイル済みヘッダーに含めるか、必要以上に頻繁に完全な再構築を行うことに注意してください。これは、STLヘッダーやその他のライブラリインクルードファイルに適した場所です。
ccacheは、キャッシングテクニックを利用して処理速度を上げる別のユーティリティです。
多くのコンパイラ/ IDEは、複数のコア/ CPUを使用して同時にコンパイルすることをサポートしています。ではGNUのメイク(通常はGCCで使用)、使用-j [N]
オプションを選択します。Visual Studioでは、複数のプロジェクトを並行してビルドできるようにするオプションが設定の下にあります。プロジェクトレベルの並列処理だけでなく、ファイルレベルの並列処理の/MP
オプションを使用することもできます。
その他の並列ユーティリティ:
コンパイラーが最適化しようとすればするほど、動作しにくくなります。
あまり変更されていないコードをライブラリに移動すると、コンパイル時間を短縮できます。共有ライブラリ(.so
または.dll
)を使用すると、リンク時間も短縮できます。
より多くのRAM、より高速なハードドライブ(SSDを含む)、およびより多くのCPU /コアは、すべてコンパイル速度に違いをもたらします。
私は、非常にテンプレート化されたC ++ライブラリであるSTAPLプロジェクトに取り組んでいます。たまには、コンパイル時間を短縮するためにすべての手法を再検討する必要があります。ここでは、私たちが使用するテクニックをまとめました。これらのテクニックのいくつかは、すでに上記にリストされています。
シンボル長とコンパイル時間の間に実証済みの相関関係はありませんが、平均シンボルサイズを小さくすると、すべてのコンパイラでコンパイル時間を改善できることがわかっています。したがって、最初の目標は、コード内で最大のシンボルを見つけることです。
nm
コマンドを使用して、サイズに基づいてシンボルを一覧表示できます。
nm --print-size --size-sort --radix=d YOUR_BINARY
このコマンドでは--radix=d
、サイズを10進数で表示できます(デフォルトは16進数です)。次に、最大のシンボルを見て、対応するクラスを分割できるかどうかを確認し、基本クラスのテンプレート化されていない部分を因数分解するか、クラスを複数のクラスに分割して再設計してみます。
通常のnm
コマンドを実行し、それをお気に入りのスクリプト(AWK、Pythonなど)にパイプして、長さに基づいてシンボルを並べ替えることができます。私たちの経験に基づいて、この方法は、方法1よりも候補をより良くする最大の問題を特定します。
「Templightは、テンプレートのインスタンス化の時間とメモリ消費をプロファイルし、インタラクティブなデバッグセッションを実行して、テンプレートのインスタンス化プロセスを内省するためのClangベースのツールです。」
Templightをインストールするには、LLVMとClang(手順)をチェックアウトし、Templightパッチを適用します。LLVMとClangのデフォルト設定はデバッグとアサーションにあり、これらはコンパイル時間に大きな影響を与える可能性があります。Templightには両方が必要なようですので、デフォルト設定を使用する必要があります。LLVMとClangのインストールプロセスには、約1時間ほどかかります。
パッチを適用した後templight++
、インストール時に指定したビルドフォルダーにあるコードをコンパイルするために使用できます。
それtemplight++
がPATHにあることを確認してください。コンパイルするには、次のスイッチをCXXFLAGS
Makefileのに追加するか、コマンドラインオプションに追加します。
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
または
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
コンパイルが完了すると、同じフォルダーに.trace.memory.pbfと.trace.pbfが生成されます。これらのトレースを視覚化するには、これらを他の形式に変換できるTemplightツールを使用できます。templight-convertをインストールするには、次の手順に従います。通常、callgrind出力を使用します。プロジェクトが小さい場合は、GraphViz出力を使用することもできます。
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
生成されたcallgrindファイルは、kcachegrindを使用して開くことができます。ここでは、最も時間とメモリを消費するインスタンス化を追跡できます。
テンプレートのインスタンス化の数を減らすための正確な解決策はありませんが、役立ついくつかのガイドラインがあります。
たとえば、クラスがある場合、
template <typename T, typename U>
struct foo { };
そして、の両方T
とU
10の異なるオプションを持つことができ、これを解決するために、あなたが100に、このクラスの可能なテンプレートのインスタンス化を増加している一つの方法は、抽象に別のクラスへのコードの共通部分です。もう1つの方法は、継承の逆転(クラス階層の逆転)を使用することですが、この手法を使用する前に、設計目標が損なわれていないことを確認してください。
この手法を使用すると、共通セクションを一度コンパイルして、後で他のTU(翻訳単位)とリンクできます。
クラスのすべての可能なインスタンス化がわかっている場合は、この手法を使用して、すべてのケースを別の翻訳単位でコンパイルできます。
たとえば、次の場所にあります。
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
このクラスには3つのインスタンス化が考えられます。
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
上記を翻訳単位に入れ、ヘッダーファイルのクラス定義の下にexternキーワードを使用します。
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
この手法は、インスタンス化の共通セットを使用して異なるテストをコンパイルする場合に時間を節約できます。
注:MPICH2は、この時点での明示的なインスタンス化を無視し、インスタンス化されたクラスを常にすべてのコンパイル単位でコンパイルします。
ユニティビルドの全体的な考え方は、使用するすべての.ccファイルを1つのファイルに含め、そのファイルを1回だけコンパイルすることです。この方法を使用すると、さまざまなファイルの共通セクションの再インスタンス化を回避でき、プロジェクトに多くの共通ファイルが含まれている場合は、おそらくディスクアクセスも節約できます。
例として、3つのファイルfoo1.cc
、foo2.cc
がfoo3.cc
あり、それらすべてがSTLtuple
からインクルードされていると仮定します。次のようなを作成できます。foo-all.cc
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
このファイルをコンパイルするのは1回だけであり、3つのファイル間の一般的なインスタンス化を減らす可能性があります。一般に、改善が有意であるかどうかを予測することは困難です。しかし、明らかな事実の1つは、ビルドで並列処理が失われることです(3つのファイルを同時にコンパイルすることはできなくなります)。
さらに、これらのファイルのいずれかに大量のメモリが使用されると、コンパイルが完了する前に実際にメモリ不足になる可能性があります。GCCなどの一部のコンパイラでは、メモリ不足のためコンパイラにICE(内部コンパイラエラー)が発生する場合があります。そのため、長所と短所をすべて理解していない限り、この手法を使用しないでください。
プリコンパイル済みヘッダー(PCH)は、ヘッダーファイルをコンパイラーが認識できる中間表現にコンパイルすることにより、コンパイル時間を大幅に節約できます。プリコンパイル済みヘッダーファイルを生成するには、通常のコンパイルコマンドでヘッダーファイルをコンパイルするだけです。たとえば、GCCの場合:
$ g++ YOUR_HEADER.hpp
これは、生成されますYOUR_HEADER.hpp.gch file
(.gch
同じフォルダにGCCでPCHファイルの拡張子です)。これは、YOUR_HEADER.hpp
他のファイルにインクルードする場合、コンパイラーは以前に同じフォルダーにではYOUR_HEADER.hpp.gch
なく自分を使用することを意味しYOUR_HEADER.hpp
ます。
この手法には2つの問題があります。
all-my-headers.hpp
。しかし、それはすべての場所に新しいファイルを含める必要があることを意味します。幸い、GCCにはこの問題の解決策があります。-include
新しいヘッダーファイルを使用して提供します。この手法を使用して、異なるファイルをカンマで区切ることができます。例えば:
g++ foo.cc -include all-my-headers.hpp
名前なし名前空間(別名匿名名前空間)は、生成されるバイナリサイズを大幅に削減できます。名前空間は内部リンケージを使用しています。つまり、これらの名前空間で生成されたシンボルは、他のTU(翻訳またはコンパイル単位)からは見えません。コンパイラは通常、名前のない名前空間に一意の名前を生成します。これは、ファイルfoo.hppがある場合、
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
そして、あなたはたまたまこのファイルを2つのTU(2つの.ccファイルに含め、それらを別々にコンパイル)に含めます。2つのfooテンプレートインスタンスは同じではありません。これは、One Definition Rule(ODR)に違反しています。同じ理由で、名前のない名前空間をヘッダーファイルで使用することはお勧めしません。.cc
バイナリファイルにシンボルが表示されないように、ファイルで自由に使用してください。場合によっては、.cc
ファイルのすべての内部詳細を変更すると、生成されるバイナリサイズが10%減少することがわかりました。
新しいコンパイラでは、動的共有オブジェクト(DSO)でシンボルを表示または非表示に選択できます。理想的には、可視性を変更すると、コンパイラーのパフォーマンス、リンク時最適化(LTO)、および生成されるバイナリー・サイズが改善される可能性があります。GCCのSTLヘッダーファイルを見ると、広く使用されていることがわかります。可視性の選択を可能にするには、関数ごと、クラスごと、変数ごと、さらに重要なことにはコンパイラごとにコードを変更する必要があります。
可視性の助けを借りて、生成された共有オブジェクトから非公開と見なすシンボルを非表示にすることができます。GCCでは-visibility
、コンパイラのオプションにデフォルトまたは非表示を渡すことにより、シンボルの可視性を制御できます。これはある意味で名前のない名前空間に似ていますが、より複雑で煩わしい方法です。
ケースごとの可視性を指定する場合は、関数、変数、およびクラスに次の属性を追加する必要があります。
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
GCCのデフォルトの可視性はデフォルト(パブリック)です。つまり、上記を共有ライブラリ(-shared
)メソッドとしてコンパイルするfoo2
と、クラスfoo3
は他のTU foo1
からfoo4
は見えなくなります(そして表示されます)。でコンパイルすると-visibility=hidden
、foo1
表示されるだけになります。foo4
隠されることさえあります。
可視性の詳細については、GCC wikiを参照してください。
「内なるゲーム、インディーズゲームのデザインとプログラミング」の記事をお勧めします。
確かに、それらはかなり古いものです。現実的な結果を得るには、最新バージョン(または使用可能なバージョン)ですべてを再テストする必要があります。いずれにせよ、それはアイデアの良い情報源です。
過去に私にとって非常にうまく機能した1つの手法:複数のC ++ソースファイルを個別にコンパイルせず、次のように他のすべてのファイルを含む1つのC ++ファイルを生成します。
// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"
もちろん、これは、ソースのいずれかが変更された場合に、含まれているすべてのソースコードを再コンパイルする必要があることを意味するため、依存関係ツリーは悪化します。ただし、複数のソースファイルを1つの変換単位としてコンパイルすると(少なくとも、MSVCとGCCを使った実験では)高速になり、より小さなバイナリが生成されます。また、コンパイラーには最適化の可能性がより多く与えられると思います(一度により多くのコードを表示できるため)。
この手法はさまざまなケースで機能しません。たとえば、2つ以上のソースファイルが同じ名前のグローバル関数を宣言している場合、コンパイラーはベイルアウトします。私はこのテクニックを他の回答で説明することができなかったので、ここでそれについて言及します。
価値のあることとして、KDEプロジェクトは1999年以来、まったく同じ手法を使用して、最適化されたバイナリを(おそらくリリース用に)構築しました。ビルド構成スクリプトへの切り替えが呼び出されました--enable-final
。考古学的な興味から、この機能を発表した投稿を掘り下げました:http : //lists.kde.org/?l=kde-devel&m=92722836009368&w=2
<core-count> + N
でN
ある並列にコンパイルされたサブリストに分割される可能性があります(システムメモリとマシンの使用方法によって異なります)。
このトピックに関する本全体は、Large-Scale C ++ Software Design(John Lakos著)です。
この本はテンプレートよりも古いため、その本の内容に「テンプレートを使用するとコンパイラが遅くなる可能性がある」と追加されています。
私は他の回答にリンクするだけです。どうすればコンパイル時間を短縮し、Visual C ++プロジェクト(ネイティブC ++)のリンク時間を短縮できますか?。追加したいもう1つのポイントですが、多くの場合問題が発生するのは、プリコンパイル済みヘッダーを使用することです。ただし、GUIツールキットのヘッダーなど、ほとんど変更されない部分にのみ使用してください。そうでなければ、彼らは最終的にあなたを救うよりもあなたに多くの時間を費やします。
別のオプションは、GNU makeで作業するときに-j<N>
オプションをオンにすることです。
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
私は3
ここにデュアルコアを持っているので、私は通常それを持っています。次に、それらの間の依存関係がなければ、異なる翻訳単位に対してコンパイラーを並行して実行します。すべてのオブジェクトファイルをリンクするリンカプロセスが1つしかないため、リンクを並行して行うことはできません。
しかし、リンカー自体はスレッド化することができ、これがELFリンカーが行うことです。これは最適化されたスレッド化されたC ++コードで、ELFオブジェクトファイルを以前よりもはるかに高速にリンクすると言われています(実際にはbinutilsに含まれていました)。GNU gold
ld
ここに幾つかあります:
make -j2
良い例です)。-O1
比べ-O2
又は-O3
)。-j12
周り-j18
はに比べてかなり高速-j8
でした。メモリ帯域幅が制限要因になる前に、コアをいくつ持つことができるのか疑問に思います...
-j
、実際のコア数の2倍です。
上記のすべてのコードトリック(前方宣言、パブリックヘッダーへのヘッダーのインクルージョンの最小化、Pimplを使用した実装ファイル内のほとんどの詳細のプッシュ...)を適用し、言語に関して他に何も得られなくなったら、ビルドシステムを検討してください。 。Linuxを使用している場合は、distcc(分散コンパイラ)とccache(キャッシュコンパイラ)の使用を検討してください。
最初の1つであるdistccは、プリプロセッサーのステップをローカルで実行し、ネットワーク内の最初の使用可能なコンパイラーに出力を送信します。ネットワーク内のすべての構成済みノードで同じコンパイラーとライブラリーのバージョンが必要です。
後者のccacheはコンパイラキャッシュです。それは再びプリプロセッサを実行し、そのプリプロセッサファイルが同じコンパイラパラメータですでにコンパイルされているかどうかを(ローカルディレクトリに保持されている)内部データベースでチェックします。含まれている場合は、バイナリをポップアップし、コンパイラの最初の実行から出力します。
両方を同時に使用できるため、ccacheにローカルコピーがない場合は、distccを使用してそれをネットを介して別のノードに送信できます。そうでない場合は、それ以上処理せずにソリューションを注入できます。
より多くのRAM。
誰かがRAMドライブについて別の答えで話しました。私はこれを80286とTurbo C ++(年齢を表示)で行い、結果は驚異的でした。マシンがクラッシュしたときのデータの損失も同様です。
可能な場合は、前方宣言を使用してください。クラス宣言で型へのポインターまたは参照のみを使用する場合は、それを前方宣言して、型のヘッダーを実装ファイルに含めるだけです。
例えば:
// T.h
class Class2; // Forward declaration
class T {
public:
void doSomething(Class2 &c2);
private:
Class2 *m_Class2Ptr;
};
// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
// Whatever you want here
}
インクルードが少ないということは、十分に実行すれば、プリプロセッサーの作業がはるかに少ないことを意味します。
使用する
#pragma once
ヘッダーファイルの上部にあるため、翻訳単位に2回以上含まれている場合、ヘッダーのテキストは1回だけ含まれ、解析されます。
完全を期すために:ビルドシステムが愚かであるだけでなく、コンパイラーがその作業を実行するのに長い時間がかかるため、ビルドが遅くなる可能性があります。
読む再帰は有害考慮してください Unix環境では、このトピックの議論について(PDF)。
RAMドライブの使用について考えました。結局のところ、私のプロジェクトではそれほど大きな違いはないことがわかりました。しかし、それらはまだかなり小さいです。それを試してみてください!どれだけ役に立ったか聞いてみたいと思います。
動的リンク(.so)は、静的リンク(.a)よりもはるかに高速です。特に、低速のネットワークドライブがある場合。これは、処理と書き出しが必要なすべてのコードが.aファイルにあるためです。さらに、はるかに大きな実行可能ファイルをディスクに書き出す必要があります。
Linux(およびおそらく他の* NIX)では、出力をSTARINGせずに別の TTYに変更することで、コンパイルを本当に高速化できます。
これが実験です:printfがプログラムを遅くします
マルチコアプロセッサを使用している場合、Visual Studio(2005以降)とGCCの両方がマルチプロセッサコンパイルをサポートします。あなたがハードウェアを持っているなら、確かにそれは有効になるものです。
「テクニック」ではありませんが、多くのソースファイルを含むWin32プロジェクトが、「Hello World」の空のプロジェクトよりも速くコンパイルされる方法を理解できませんでした。したがって、これが私のような人に役立つことを願っています。
Visual Studioでは、コンパイル時間を増やす1つのオプションはインクリメンタルリンク(/ INCREMENTAL)です。リンク時コード生成(/ LTCG)と互換性がないため、リリースビルドを実行するときはインクリメンタルリンクを無効にしてください。
/INCREMENTAL
デバッグモードでのみ有効にする必要があります