この記事は、Yılmaz Yörü氏のhttps://blogs.embarcadero.com/cbuilder-optimization-guide-with-twinecompile/の抄訳です
このブログでは、C++Builderプロジェクトを最適化し、できるだけ速くコンパイルするためのヒントやコツをご紹介いたします。 ここでは、TwineCompileでの使用を想定した最適化について説明しますが、これらのテクニックの一部はC++Builder全般に適用できます。
C++BuilderとTwineCompileには、コンパイル速度を最適化するための機能が数多く搭載されていますが、プロジェクトごとに条件や設定が異なるため、チューニングが必要になることがよくあります。ここでは、プロジェクトの種類や構造ごとに、いつ、どのように適用すればよいのか、さまざまなヒントを見ていきます。
Table of Contents
トリック #1 – TwineCompile
最初のトリックは簡単で、TwineCompileを使い始めることです。TwineCompileはC++Builderに統合されるIDEプラグインで、C++Builderコンパイラ(従来のコンパイラおよびCLANGコンパイラの両方)にマルチスレッド、キャッシング、その他の強力な機能を追加します。場合によっては、TwineCompileをインストールするだけで、最新世代のビルドワークステーションでのプロジェクトのコンパイル速度が50倍になることもあります。
TwineCompileは、C++Builder 10.4.1以降のバージョンをご利用で、有効なアップデートサブスクリプションをお持ちのお客様であれば、GetIt パッケージマネージャから無償で入手できます。プロジェクトの構造やファイル(次のヒントとは異なりますが)に変更を加えることなく、パフォーマンスを向上させることができるため、コンパイル時間を短縮するための最初の手段となるはずです。
TwineCompileを入手するには、IDEでGetItパッケージマネージャを開き、TwineCompileを検索します。 指示に従ってインストールしてください。
詳しくは、以下のブログもご覧ください。
TwineCompileをIDEに統合すると、IDEのコンパイル/メイク/ビルドコマンドを自動的にフックし、IDEのビルドプロセスの代わりにTwineCompileを起動します。これにより、現在のようにIDEを使い続けながら、TwineCompileが提供するコンパイル性能を最大限に活用することができます。
トリック #2 – CLANGでPCH (プリコンパイル済みヘッダー)を使用
C++Builderには、コンパイル時間が遅い場合、それを改善するための秘訣があります。最も効果的な方法は、プロジェクトにプリコンパイル済みヘッダーを適切に設定して使用することです。CLANGコンパイラの導入により、この機能が大幅に改善され、より使いやすくなりました。
現在、CLANGコンパイラを使用しているプロジェクトの場合は、コンパイル時間に大きな違いが生じるため、CLANGのPCHサポートを利用することを強くお勧めします。またプロジェクトで、まだ従来のBorlandコンパイラを使用している場合は、CLANGコンパイラを使用することをお勧めします。C++v17をサポートした標準準拠の最新のC++コンパイラを使用できるだけでなく、CLANG PCH システムも使用できます。
ClangのPCHの詳細は、以下のドキュメントを参照ください。
Clangコンパイラでは、プロジェクトは一度に1つのプリコンパイル済みヘッダーのみ使用できます。
プリコンパイル済みヘッダーのコンセプトは非常にシンプルで、プロジェクトのほとんど(すべてではないにしても)のユニットで使用されるヘッダーのセットを取得し、それらをバイナリデータにコンパイルします。プロジェクトのユニットにこれらのプリコンパイル済みヘッダーがインクルードされている場合、コンパイラはヘッダーを最初から処理/コンパイルする必要はなく、プリコンパイル済みのデータをディスクからロードするだけで、ユニット内のコードのコンパイルに進むことができます。
コンパイラがプリコンパイル済みヘッダーを確実に使用できるように、各ユニットのヘッダー順序を同一にする必要があるため、ある程度の規律が必要です。さらに、プリコンパイル済みヘッダーには、頻繁に変更されるヘッダーを含めるべきではありません。これは、ヘッダーをコンパイルする際のオーバーヘッドがかなり大きいためで、常に再構築しなければならない場合には、プリコンパイルされたヘッダーの利点をすべて失うことになります。
ステップ #1
まず、プロジェクトを分析して、ほとんどのユニットがインクルードしているヘッダーを特定することから始めます。そのためには、ほとんどのユニットまたはすべてのユニットが同じヘッダーセットをインクルードするための再編成が必要になるかもしれません。なお、未使用のヘッダーを追加しても悪影響はありませんので、すべてのヘッダーを使用しないユニットがあっても、まったく問題ありません。
プリコンパイル済みのヘッダーが多ければ多いほど、コンパイル時間の改善に効果的です。
ステップ #2
プロジェクトに既にPCHヘッダーファイルがある場合(新しいプロジェクト用にIDEによって自動生成されたPCH .hファイルなど)、ステップ #6に進んでください。
プロジェクトにPCHヘッダーファイル(新しいプロジェクト用にIDEによって自動生成されたPCH .hファイルなど)がない場合は、プロジェクトに新しいヘッダーファイルを追加します。
ステップ #3
新しいヘッダーファイルは、ProjectNamePCH.hのようなわかりやすい名前で保存します。
ステップ #4
[プロジェクト]ペインでヘッダーファイルを右クリックし、[プリコンパイルに使用]オプションを選択します。
ステップ #5
このプリコンパイル用ヘッダーファイルを開き、ステップ #1で分析し、プロジェクトで使用されているすべてのinclude文を配置します。例えば、以下のようになります。
#include <vcl.h>
#include <tchar.h>
ステップ #6
プロジェクトのすべてのユニットを見て、PCHヘッダーにあるヘッダーを参照している#includeステートメントを削除します。各ユニットの先頭で、他のステートメントの前に、以下のpragma指令を定義して、そのユニットがコンパイル済みのヘッダーを調整していないことをコンパイラに通知します。
#pragma hdrstop
ステップ #7
プロジェクトオプションを開き、「C++コンパイラ」の「プリコンパイル済みヘッダー」セクションで、「PCHの使用法」オプションの「生成し使用する」を選択します。
ステップ #8
プロジェクトをビルドします。 PCHヘッダーファイルをバイナリデータにコンパイルするのに時間がかかりますが、その後、これらのヘッダーをユニットごとに何度も処理する必要がなくなるため、後続のユニットは何倍も高速にコンパイルされます。
トリック #3 – TwineCompile Classicコンパイラ向けの単一ヘッダーのPCH
あなたのプロジェクトがまだ従来のBorlandコンパイラ(以降、クラシックコンパイラ)を使用していて、新しいCLANGコンパイラに移行できない場合でも、クラシックコンパイラは驚くほど強力なPCHシステムを備えているため、プリコンパイル済みヘッダーを使用することで、コンパイル速度を大幅に向上させることができます。
TwineCompileは、このシステムを最大限に活用するようにビルドされており、プリコンパイル済みヘッダーを使用している各ファイルに対して、適切なパラメータでクラシックコンパイラが自動的に呼び出されるようにビルドプロセスを調整します。
CLANGコンパイラとは異なり、クラシックコンパイラは、通常のソースファイルのコンパイルの一部としてプリコンパイル済みヘッダーを生成して使用するように設計されています。プリコンパイル済みのヘッダーとして指定されたファイルをコンパイルするための別のコンパイル手順は必要ありません。
Clangコンパイラでは、プロジェクトごとに単一ヘッダーのPCHしか使用できないというルールがありますが、クラシックコンパイラでは、複数のコンパイル済みヘッダーが使用できます。今回はクラシックコンパイラでも単一ヘッダーを使用するように変更してみましょう。
クラシックコンパイラで単一ヘッダーのPCHを使用するようにプロジェクトを設定するには、2つの方法があります。
- インジェクションコマンドを使用して、コンパイルプロセスの一部として単一ヘッダーのPCHファイルをすべてのユニットに自動的に含める
- 単一ヘッダーのPCHファイルの#include文をすべてのユニットの先頭に手動で追加する。
それでは、実際にクラシックコンパイラで単一ヘッダーのPCHを使用する手順をみてみましょう。
ステップ #1
CLANGコンパイラの手順と同様に、まずプロジェクトを分析し、ほとんどのユニットに含まれているヘッダーを特定することから始めます。そのためには、ほとんどのユニットまたはすべてのユニットが同じヘッダーセットを含むようにするための再編成が必要になるかもしれません。なお、未使用のヘッダーを追加しても悪影響はありませんので、すべてのヘッダーを使用しないユニットがあっても、まったく問題ありません。
プリコンパイル済みのヘッダーが多ければ多いほど、コンパイル時間の改善に効果的です。
ステップ #2
プロジェクト内に新しいヘッダーファイルを作成します。
ステップ #3
新しいヘッダーファイルは、ProjectNamePCH.hのようなわかりやすい名前で保存します
ステップ #4
このプリコンパイル用ヘッダーファイルを開き、ステップ #1で集めたすべてのinclude文を配置します。例えば、以下のようになります。
#include <vcl.h>
#include <tchar.h>
ステップ #5a
方法A – インジェクション(プリコンパイル済みヘッダーファイルの挿入)の機能を使用したい場合は、プロジェクトオプションを開き、「C++コンパイラ」の「プリコンパイル済みヘッダー」セクションに移動し、「プリコンパイル済みヘッダーファイルを挿入する」オプションにヘッダーファイル名を入力します。
ステップ #5b
方法B – ヘッダーを手動でユニットに追加したい場合は、このヘッダーファイルの#include文に続けて、#pragma hdrstop行をユニットの一番上に配置します。例えば、以下のようになります。
#include "ProjectNamePCH.h"
#pragma hdrstop
ステップ #6
プロジェクトのオプションを開き、「C++コンパイラ」の「プリコンパイル済みヘッダー」セクションに進み、以下の設定を行います。
- PCHを使用するには、[PCHの使用法]オプションの”生成して使用”を選択します。 これにより、TwineCompileは、最初のソースコードファイルを使用してPCHファイルを生成し、それを後続のすべてのユニットで使用するように指示されます。
- 「プリコンパイル済みヘッダーをキャッシュする」のtrueにチェックする
- 生成されるPCHファイルの名前を「PCH ファイル名(‘従来’のBorlandコンパイラ」オプションに入力します。(例:Project.csm) このファイルには、プリコンパイル済みヘッダーとして選択されたヘッダーからコンパイルされたバイナリデータが含まれます。
設定したオプションの例は、以下の通りです。
ステップ #7
プロジェクトをビルドします。TwineCompileは、コンパイルされるファイルのコンパイラオプションを自動的に調整して、PCHファイルが生成されてコンパイルプロセスの一部として使用されるようになり、コンパイル時間が大幅に改善されます。
トリック #4 – TwineCompile Classicコンパイラ向けのPCHを自動的に作成
CLANGコンパイラに移行できないプロジェクトや、すべてのユニットで動作する単一のPCHファイルをサポートできないプロジェクト構造でも、まだ代替方法があります。
クラシックコンパイラには、TwineCompileと組み合わせた高度なPCH機能がいくつかあり、このようなタイプのプロジェクトに対して、チューニングされたプリコンパイル済みヘッダーのパフォーマンス上での利点のほとんどを提供することができます。
クラシックコンパイラは、「適応型」(公式な名称ではありませんが)プリコンパイル済みヘッダーのコンセプトをサポートしています。プロジェクトのユニットが使用する様々なバリエーションのヘッダーをサポートするために、プロジェクトのビルドの一部として継続的に作成、調整されます。問題は、これを自分のプロジェクトで動作させることですが、ここでTwineCompileの出番となります。
ステップ #1
プロジェクトのオプションを開き、「C++コンパイラ」の「プリコンパイル済みヘッダー」セクションに進みます。
- PCHを使用するには、[PCHの使用法]オプションの”生成して使用”を選択します。 このプロジェクトにはPCH構造がないため、通常、このオプションは、解決するよりも多くの問題を引き起こす可能性がありますので注意してください。
- 「プリコンパイル済みヘッダーをキャッシュする」のtrueにチェックする
- 生成されるPCHファイルの名前を「PCH ファイル名(‘従来’のBorlandコンパイラ」オプションに入力します。(例:Project.csm) このファイルには、プリコンパイル済みヘッダーとして選択されたヘッダーからコンパイルされたバイナリデータが含まれます。
設定したオプションの例は、以下の通りです。
ステップ #2
TwineCompileメニューから「Options」を開き、「Pre-Compiled Headers」で「Use PCH file for each thread」オプションにチェックし、「OK」をクリックします。
ステップ #3
プロジェクトをビルドします。TwineCompileは、各ファイルのコンパイラオプションを自動的に調整し、各ユニットにインクルードされるヘッダーの異なるバリエーションに適応するPCHファイルを生成して使用できるようにします。パフォーマンスは、適切なPCH設定ほどではありませんが、PCHを全く使用しない場合に比べて格段に良くなります。
トリック #5 – プロジェクトレイアウトの最適化
このトリックを別の言い方をすれば、変更時にコンパイルする必要のあるファイルの数を最小限に抑えながら、リンクに要する時間とのバランスを取ることができます。
C++のコンパイル時間を最適化するためによく使われる手法は、プロジェクトを多数の小さなライブラリまたはパッケージに分割することです。これは、コードが変更されても、プロジェクト内のいくつかのファイルだけを再コンパイルすれば良いという考え方です。再コンパイルされたライブラリは、他のライブラリと再リンクするだけで最終的な結果が得られます。
特にTwineCompileを使用している場合、アンチパターンとなる原因が2つあります。
- コンパイルとリンクに要する時間が逆のパターンです。このアプローチを推奨するアドバイスは、一般的にgccのような遅いC++コンパイラを対象としています。gccは非常にコンパイル時間は遅いですが、その反面リンク時間は速いです。そこで、その作業をリンカに任せることが有効です。C++Builderのコンパイラ(CLANGとClassicの両方)はC++コンパイラとしてはかなり高速なので、例えば、30個のC++ファイルのコンパイルを回避するために15個のリンクステップを追加すると、リンカの実行がボトルネックとなりパフォーマンスが大幅に低下することがよくあります。
- リンクはシリアルプロセスで、コンパイルはパラレルプロセスです。TwineCompileを使用している場合、C++ファイルは並行してコンパイルできるため、同時にコンパイルできるユニット数が多ければ多いほど、より高いパフォーマンスを実現することができます。例えば、200個のC++ユニットで構成された単一のプロジェクトを1回リンクする所要時間と、20個のC++ユニットで構成されたプロジェクトが10個あり、それぞれのプロジェクトを1回ずつリンクさせ、その合計の所要時間をそれぞれ比較すると、前者のほうが何倍も速くビルドすることができます。
分割されたプロジェクト構造が理にかなっているケースもありますが、それはトレードオフでもあります。残念ながら、プロジェクトをどのように構成すべきか、唯一の正解はありませんので、以下のガイドラインをご紹介します。
- ソースに変更があった場合、コンパイルしなければならないファイルの数を最小限に抑える
- リンクする必要のあるライブラリ、パッケージ、またはアプリケーションの数を最小限に抑える。 静的ライブラリであっても、リンクはかなり高速ですが、オーバーヘッドがあります。
- プロセッサのリソースを最大限に活用する。リンカは1つのコアしか使いませんが、例えば、8コア+HTプロセッサで16個のC++ファイルを実行すると、16個の論理コアすべてを使用します。したがって、これらの16個のファイルをコンパイルすると、リンカよりも16倍多くのリソースを使用できます。
Trick #6 – SORTA コンパイラ
TwineCompileの最も強力な機能の1つに、自動バックグラウンドコンパイルシステムがあります。基本的にこの機能は、変更したソースコードファイルをバックグラウンドで自動的にコンパイルするため、「メイク」や「実行」をクリックしたときに、コンパイルするファイルがなく、すべてが最新の状態になっており、ビルドプロセスではアプリケーションをリンクするだけで済みます。
ここまで上述してきた多くのトリックとは異なり、このトリックはコンパイルまたはビルドプロセスを高速化することに直接関係していませんが、ソースコードをコンパイルする必要性を減らす、あるいは無くすことを目的としています。
SORTA Compileを有効にするには、TwineCompile Optionsを開き、[SORTA Compile]セクションに移動して、”Enable SORTA Compile”オプションにチェックします。
この機能を呼び出すために使用される条件は、
- ユニットが保存された時
- エディタのタブが変更された時
- ファイルの変更後、一定期間、他から変更が無いとき
など、いくつかのトリガーが存在します。
SORTA Compileを効果的に使用するためには、コーディングのワークフローを調整する必要がありますが、従来の4段階のプロジェクト進化プロセス(編集、コンパイル、実行、テスト、その繰り返し)に大きなメリットをもたらします。
トリック #7 – スタティックリンク vs ダイナミックリンク
トリック#6をプロジェクトに適用すると、コンパイルはバッググラウンドで行われるため、コンパイルを待つ時間よりも、リンカを待つ時間のほうが長く感じるかもしれません。ただし、これには一長一短があります。常にコンパイル時間を節約できることは良いことですが、ビルドのパフォーマンスを最適化するためにできることが、他に選択肢が無いためです。
しかし、一部のアプリケーションでは、異なる機能を別々のDLLに格納し、実行時にアプリケーションがロードするように再構築することができます。つまり、アプリケーションの特定の部分にコードが変更されると、その特定のコードのみがコンパイルされ、独自のDLLファイルにリンクされます。アプリケーションは実行時にこのDLLにリンクしているため、アプリケーションの残りの部分を再構築する必要はありません。
もちろん、この方法では、アプリケーションにバンドルする必要のあるDLLファイルが増えるため、デプロイと管理のオーバーヘッドが発生しますが、ビルドのパフォーマンスを向上させることができるため、オーバーヘッドを増やす価値は十分にあります。
トリック #8 – ハードウェア
最後にご紹介するのは、古くからよく知られているトリックで、「問題にハードウェアを投入して解決する」という手法です。これまでは、より新しく、より高速なハードウェアを使用しても、それに対するメリットは、あまり多くの見返りは得られませんでしたが、今日の最新のハードウェアは、TwineCompileのシステムリソースを最大限に利用する機能と相まって、いくつかの大きなメリットを得ることができます。
注目すべき点は2つあります。
1. Disk performance
これは、NVMe/PCIe接続を使用したSSDドライブを意味します。SATA SSDも良いのですが、PCIeバスで動作するSSDに比べてランダムアクセス性能が不足しています。
ハードディスクドライブのようなスピンドル媒体は絶対に避けたいものです。プロジェクト、一時ディレクトリ、オペレーティング・システム、またはC++Builderのインストール・パスがスピンドル上にあると、ビルドのパフォーマンスが極端に低下します。
2. CPU cores
最近では、C++のコンパイルにおけるシングルコアの直線的な性能は、多くのコアを使用するよりも有用性が低くなっています。利用可能な最大数のコアを利用してください。
64コア(128スレッド)のAMD Threadripperプロセッサは、TwineCompileを使ってC++BuilderでC++をコンパイルするのに最適です。このようなプロセッサでは、プロジェクト内のC++ファイル数が128個に満たない場合、CPUリソースを無駄に消費していることに注意してください。これはトリック #5で述べた、同一プロジェクト内の多くのファイルをコンパイルすることでCPUリソースを最大限に利用するために、プロジェクトのレイアウトを最適化するという点にもつながります。
CPUのコア数に応じて、TwinComplileによるC++の並列コンパイル速度が劇的に変化する情報を過去のブログで紹介しています。詳しくは以下を参照してください。
まとめ
ビルドに時間がかかり、どうすることもできなかった昔のC++コンパイルの時代から、ずいぶんと進歩してきました。あらゆる規模のプロジェクトに大きな違いをもたらすツールやテクニックが開発されています。このブログでは、そのようなツールやテクニックをいくつか紹介していますが、C++開発エクスペリエンスをより速く、より簡単に、より簡単にするために、それらを独自のプロジェクトに適用できることを願っています。
補足
このブログは、C++Builderの英語版とTwineComplieでの利用を前提に説明しておりますが、C++Builderの日本語版で利用する際はご注意ください。C++Builderの日本語版で、クラシックコンパイラ(bcc32)とTwineComplieの組み合わせで利用した場合、コンパイラエラーが発生した際、エラーメッセージ等が文字化けする症状が報告されております。ただし、この症状はClangコンパイラ(bcc32c、bcc64)とTwineComplieの組み合わせでは、同様の問題は発生いたしません。 ただTwineComplieは、ビルド時間がかかるClangコンパイラと併用することで真価を発揮し、コンパイルのパフォーマンスを向上させるため、Clangコンパイラでの利用を推奨いたします。