44

私はSOを使用してこれに対する答えを見つけようとしました。C ++でヘッダーのみのライブラリを構築することのさまざまな長所と短所をリストした質問がいくつかありますが、定量化できる用語でそうするものを見つけることができませんでした。

それで、定量化可能な用語で、従来分離されたc ++ヘッダーと実装ファイルを使用することとヘッダーのみを使用することの違いは何ですか?

簡単にするために、テンプレートは使用されていないと仮定します(ヘッダーのみが必要なため)。

詳述するために、私は記事から私が見たものを賛否両論としてリストしました。明らかに、一部は簡単に定量化できないため(使いやすさなど)、したがって定量化可能な比較には役に立ちません。定量化可能なメトリックを期待するものに(定量化可能)のマークを付けます。

ヘッダーのみの長所

  1. ビルドシステムでリンカーオプションを指定する必要がないため、含める方が簡単です。
  2. ライブラリの関数はコードにインライン化されるため、常にすべてのライブラリコードを残りのコードと同じコンパイラ(オプション)でコンパイルします。
  3. それははるかに速いかもしれません。(定量化可能)
  4. コンパイラー/リンカーに最適化のより良い機会を与える可能性があります(可能であれば説明/定量化可能)
  5. とにかくテンプレートを使用する場合は必須です。

ヘッダーのみの短所

  1. それはコードを膨らませます。(定量化可能)(実行時間とメモリフットプリントの両方にどのように影響しますか)
  2. コンパイル時間が長くなります。(定量化可能)
  3. インターフェイスと実装の分離の喪失。
  4. 循環依存関係を解決するのが難しい場合があります。
  5. 共有ライブラリ/DLLのバイナリ互換性を防ぎます。
  6. これは、C++の従来の使用方法を好む同僚を悪化させる可能性があります。

大規模なオープンソースプロジェクト(同様のサイズのコードベースを比較)から使用できる例は、非常にありがたいです。または、ヘッダーのみのバージョンと分離されたバージョンを切り替えることができるプロジェクトを知っている場合(両方を含む3番目のファイルを使用)、それが理想的です。逸話的な数字は、私が洞察を得ることができる球場を私に与えるので、また役に立ちます。

長所と短所のソース:

前もって感謝します...

アップデート:

後でこれを読んでいて、リンクとコンパイルに関する背景情報を少し知りたいと思っている人にとって、私はこれらのリソースが役立つと思いました。

更新:(以下のコメントに応えて)

答えが異なるかもしれないからといって、測定が役に立たないという意味ではありません。ある時点で測定を開始する必要があります。そして、あなたが持っている測定値が多ければ多いほど、画像はより鮮明になります。この質問で私が求めているのは、全体の話ではなく、全体像を垣間見ることです。確かに、偏見を非倫理的に促進したいのであれば、誰でも数字を使って議論を歪めることができます。しかし、誰かが2つのオプションの違いに興味があり、それらの結果を公開する場合、情報は役立つと思います。

誰もこのトピックに興味がなく、それを測定するのに十分ですか?

シュートアウトプロジェクトが大好きです。これらの変数のほとんどを削除することから始めることができます。1つのバージョンのLinuxで1つのバージョンのgccのみを使用してください。すべてのベンチマークに同じハードウェアのみを使用してください。複数のスレッドでコンパイルしないでください。

次に、以下を測定できます。

  • 実行可能サイズ
  • ランタイム
  • メモリフットプリント
  • コンパイル時(プロジェクト全体と1つのファイルの変更の両方)
  • リンク時間
4

3 に答える 3

36

まとめ(注目点):

  • ベンチマークされた2つのパッケージ(1つは78のコンパイル単位、もう1つは301のコンパイル単位)
  • 従来のコンパイル(マルチユニットコンパイル)では、アプリケーションが7%高速になりました(78ユニットパッケージ)。301ユニットパッケージのアプリケーションランタイムに変更はありません。
  • 従来のコンパイルとヘッダーのみのベンチマークはどちらも、実行時に同じ量のメモリを使用していました(両方のパッケージで)。
  • ヘッダーのみのコンパイル(単一ユニットコンパイル)の結果、実行可能ファイルのサイズは、301ユニットパッケージでは10%小さくなりました(78ユニットパッケージではわずか1%小さくなりました)。
  • 従来のコンパイルでは、メモリの約3分の1を使用して、両方のパッケージをビルドしていました。
  • 従来のコンパイルでは、コンパイルに3倍の時間がかかり(最初のコンパイル時)、再コンパイルにかかる時間はわずか4%でした(ヘッダーのみがすべてのソースを再コンパイルする必要があるため)。
  • 従来のコンパイルでは、最初のコンパイルと後続のコンパイルの両方でリンクするのに時間がかかりました。

Box2Dベンチマーク、データ:

box2d_data_gcc.csv

ボタンベンチマーク、データ:

botan_data_gcc.csv

Box2Dの概要(78ユニット)

ここに画像の説明を入力してください

ボタンサマリー(301ユニット)

ここに画像の説明を入力してください

素敵なチャート:

Box2D実行可能サイズ:

Box2D実行可能サイズ

Box2Dのコンパイル/リンク/ビルド/実行時:

Box2Dのコンパイル/リンク/ビルド/実行時間

Box2Dのコンパイル/リンク/ビルド/実行の最大メモリ使用量:

Box2Dコンパイル/リンク/ビルド/実行最大メモリ使用量

Botan実行可能サイズ:

Botan実行可能サイズ

Botanのコンパイル/リンク/ビルド/実行時:

Botanのコンパイル/リンク/ビルド/実行時間

Botanコンパイル/リンク/ビルド/実行最大メモリ使用量:

Botanコンパイル/リンク/ビルド/実行最大メモリ使用量


ベンチマークの詳細

TL; DR


テストされたプロジェクト、Box2DBotanが選択されたのは、それらが潜在的に計算コストが高く、かなりの数のユニットを含み、実際には単一ユニットとしてコンパイルするエラーがほとんどまたはまったくなかったためです。他の多くのプロジェクトが試みられましたが、1つのユニットとしてコンパイルするために「修正」するには時間がかかりすぎました。メモリフットプリントは、定期的にメモリフットプリントをポーリングし、最大値を使用して測定されるため、完全に正確ではない可能性があります。

また、このベンチマークでは、ヘッダーの依存関係の自動生成は行われません(ヘッダーの変更を検出するため)。別のビルドシステムを使用するプロジェクトでは、これによりすべてのベンチマークに時間がかかる場合があります。

ベンチマークには3つのコンパイラがあり、それぞれ5つの構成があります。

コンパイラ:

  • gcc
  • icc
  • clang

コンパイラー構成:

  • デフォルト-デフォルトのコンパイラオプション
  • 最適化されたネイティブ--O3 -march=native
  • サイズ最適化--Os
  • LTO / IPOネイティブ-clang-O3 -flto -march=nativeとgcc、-O3 -ipo -march=nativeicpc/iccを使用
  • ゼロ最適化--Os

これらはそれぞれ、シングルユニットビルドとマルチユニットビルドの比較に異なる影響を与える可能性があると思います。私はLTO/IPOを含めたので、単一ユニットの有効性を達成するための「適切な」方法がどのように比較されるかを見ることができます。

csvフィールドの説明:

  • Test Name-ベンチマークの名前。例:Botan, Box2D
  • テスト構成-このテストの特定の構成に名前を付けます(特別なcxxフラグなど)。通常はと同じTest Nameです。
  • Compiler-使用するコンパイラの名前。例:gcc,icc,clang
  • Compiler Configuration-使用されるコンパイラオプションの構成の名前。例:gcc opt native
  • Compiler Version String-コンパイラ自体からのコンパイラバージョンの出力の最初の行。例:私のシステムでg++ --version生成g++ (GCC) 4.6.1します。
  • Header only-Trueこのテストケースが単一ユニットとして構築されたFalse場合、複数ユニットプロジェクトとして構築された場合の値。
  • Units-単一のユニットとして構築されている場合でも、テストケース内のユニットの数。
  • Compile Time,Link Time,Build Time,Run Time-聞こえるように。
  • Re-compile Time AVG,Re-compile Time MAX,Re-link Time AVG,Re-link Time MAX,Re-build Time AVG,Re-build Time MAX-単一のファイルに触れた後、プロジェクトを再構築するまでの時間。各ユニットに触れ、それぞれについてプロジェクトを再構築します。最大時間と平均時間がこれらのフィールドに記録されます。
  • Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size-彼らが聞こえるように。

ベンチマークを再現するには:

  • ブルワークはrun.pyです。
  • psutilが必要です(メモリフットプリント測定用)。
  • GNUMakeが必要です。
  • 現状では、パスにgcc、clang、icc/icpcが必要です。もちろん、これらのいずれかを削除するように変更できます。
  • 各ベンチマークには、そのベンチマークの単位をリストしたデータファイルが必要です。次に、 run.pyは2つのテストケースを作成します。1つは各ユニットが個別にコンパイルされ、もう1つは各ユニットが一緒にコンパイルされます。例:box2d.data。ファイル形式は、次のキーを持つ辞書を含むjson文字列として定義されます
    • "units"c/cpp/cc-このプロジェクトのユニットを構成するファイルのリスト
    • "executable"-コンパイルする実行可能ファイルの名前。
    • "link_libs"-リンクするインストール済みライブラリのスペース区切りのリスト。
    • "include_directores"-プロジェクトに含めるディレクトリのリスト。
    • "command"-オプション。ベンチマークを実行するために実行する特別なコマンド。例えば、"command": "botan_test --benchmark"
  • すべてのC++プロジェクトでこれを簡単に実行できるわけではありません。単一のユニットに競合/あいまいさがあってはなりません。
  • テストケースにプロジェクトを追加するには、データファイル名などのプロジェクトの情報を使用してrun.pytest_base_casesのリストを変更します。
  • すべてが正常に実行されている場合、出力ファイルdata.csvにはベンチマーク結果が含まれている必要があります。

棒グラフを作成するには:

  • ベンチマークによって作成されたdata.csvファイルから始める必要があります。
  • chart.pyを取得します。matplotlibが必要です。
  • リストを調整して、fields作成するグラフを決定します。
  • を実行しますpython chart.py data.csv
  • これで、ファイルにtest.png結果が含まれるはずです。

Box2D

  • Box2Dは、svnからそのまま、リビジョン251で使用されました。
  • ベンチマークはここから取得され、ここで変更されおり、優れたBox2Dベンチマークを表していない可能性があり、このコンパイラベンチマークを正当化するのに十分なBox2Dを使用していない可能性があります。
  • box2d.dataファイルは、すべての.cppユニットを見つけることによって手動で書き込まれました。

ボタン

  • Botanの使用-1.10.3
  • データファイル:botan_bench.data
  • 最初に実行./configure.py --disable-asm --with-openssl --enable-modules=asn1,benchmark,block,cms,engine,entropy,filters,hash,kdf,mac,bigint,ec_gfp,mp_generic,numbertheory,mutex,rng,ssl,stream,cvcすると、ヘッダーファイルとMakefileが生成されます。
  • 関数の境界が最適化をブロックしない場合に発生する可能性のある最適化とアセンブリが干渉する可能性があるため、アセンブリを無効にしました。ただし、これは推測であり、完全に間違っている可能性があります。
  • 次にgrep -o "\./src.*cpp" Makefile、やのようなコマンドを実行grep -o "\./checks.*" Makefileして.cppユニットを取得し、それらをbotan_bench.dataファイルに配置します。
  • Botan /checks/checks.cpptypedefとopensslの競合のため、x509ユニットテストを呼び出さないように変更し、x509チェックを削除しました。
  • Botanソースに含まれているベンチマークが使用されました。

システム仕様:

  • OpenSuse 11.4、32ビット
  • 4GBのRAM
  • Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz
于 2012-11-27T21:05:57.050 に答える
28

アップデート

これがRealSlawの最初の答えでした。上記の彼の答え(受け入れられたもの)は彼の2回目の試みです。彼の2回目の試みは完全に質問に答えていると思います。-ホーマー6

さて、比較のために、「ユニティビルド」(グラフィックエンジンとは関係ありません)のアイデアを調べることができます。基本的に、「ユニティビルド」とは、すべてのcppファイルを1つのファイルに含め、それらすべてを1つのコンパイルユニットとしてコンパイルすることです。AFAICTとして、これはプロジェクトをヘッダーのみにするのと同じです。あなたがリストした2番目の「詐欺」に驚かれることでしょう。「ユニティビルド」の要点は、コンパイル時間を短縮することです。おそらく、ユニティビルドは次の理由でコンパイルが速くなります。

..は、ビルドのオーバーヘッドを減らす方法(具体的には、ファイルの開閉と、生成されるオブジェクトファイルの数を減らすことによるリンク時間の短縮)であり、ビルド時間を大幅に短縮するために使用されます。

altdevblogaday

コンパイル時の比較(ここから):

ここに画像の説明を入力してください

「ユニティビルド」の3つの主要なリファレンス:

リストされている長所と短所の理由が必要だと思います。

ヘッダーのみの長所

[...]

3)それははるかに速いかもしれません。(定量化可能)コードはより適切に最適化される可能性があります。その理由は、ユニットが分離している場合、関数は単なる関数呼び出しであるため、そのままにしておく必要があるためです。この呼び出しに関する情報は不明です。次に例を示します。

  • この関数はメモリを変更しますか(したがって、それらの変数/メモリを反映するレジスタは、返されるときに古くなります)?
  • この関数はグローバルメモリを調べますか(したがって、関数を呼び出す場所を並べ替えることはできません)

さらに、関数の内部コードわかっている場合は、それをインライン化する(つまり、呼び出し元の関数にコードを直接ダンプする)価値があるかもしれません。インライン化により、関数呼び出しのオーバーヘッドが回避されます。インライン化により、他の多くの最適化を実行することもできます(たとえば、定数の伝播。たとえばfactorial(10)、コンパイラがのコードを知らない場合は、を呼び出します。factorial()ただし、ソースがわかっている場合は、そのままにしておく必要があります。のコードではfactorial()、実際に関数内の変数を変数化して10に置き換えることができます。運が良ければ、実行時に何も実行せずに、コンパイル時に答えを得ることができます)。インライン化後の他の最適化には、デッドコードの除去と(おそらく)より良い分岐予測が含まれます。

4)コンパイラ/リンカーに最適化のより良い機会を与える可能性があります(可能であれば説明/定量化可能)

これは(3)から続くと思います。

ヘッダーのみの短所

1)コードを肥大化させます。(定量化可能)(実行時間とメモリフットプリントの両方にどのように影響しますか)ヘッダーのみは、私が知っているいくつかの方法でコードを肥大化させる可能性があります。

1つ目はテンプレートの肥大化です。ここで、コンパイラーは、使用されないタイプの不要なテンプレートをインスタンス化します。これはヘッダーのみに特有のものではなく、テンプレートに特有のものであり、最近のコンパイラーはこれを改善して、懸念を最小限に抑えています。

2番目のより明白な方法は、関数の(過剰な)インライン化です。大きな関数が使用されるすべての場所にインライン化されると、それらの呼び出し関数のサイズが大きくなります。これは、何年も前に実行可能ファイルのサイズと実行可能ファイルのイメージメモリのサイズに関する懸念であったかもしれませんが、HDDのスペースとメモリが大きくなり、ほとんど気にする必要がなくなりました。より重要な問題は、この増加した関数サイズが命令キャッシュを台無しにする可能性があることです(そのため、現在は大きい関数がキャッシュに収まらず、CPUが関数を実行するときにキャッシュを補充する必要があります)。インライン化後、レジスタ圧力が増加します(レジスタ数に制限があります)、CPUが直接処理できるオンCPUメモリ)。これは、変数が多すぎるため、コンパイラが現在より大きな関数の途中でレジスタを調整する必要があることを意味します。

2)コンパイル時間が長くなります。(定量化可能)

まあ、ヘッダーのみのコンパイルは、多くの理由で論理的にコンパイル時間を長くする可能性があります(「ユニティビルド」のパフォーマンスにもかかわらず、ロジックは必ずしも他の要因が関与する現実世界ではありません)。1つの理由は、プロジェクト全体がヘッダーのみの場合、増分ビルドが失われることです。つまり、プロジェクトの任意の部分を変更すると、プロジェクト全体を再構築する必要がありますが、個別のコンパイルユニットを使用する場合、1つのcppを変更すると、オブジェクトファイルを再構築し、プロジェクトを再リンクする必要があります。

私の(逸話的な)経験では、これは大ヒットです。ヘッダーのみは、一部の特殊なケースでパフォーマンスを大幅に向上させるだけですが、生産性に関しては、通常、それだけの価値はありません。より大きなコードベースを取得し始めると、最初からコンパイルするのに毎回10分以上かかる場合があります。小さな変更で再コンパイルすると、面倒になり始めます。私が「;」を何回忘れたかわかりません それについて聞くには5分待たなければならず、戻って修正するだけでした。その後、「;」を修正して紹介した他の何かを見つけるためにさらに5分待たなければなりませんでした。

パフォーマンスは素晴らしく、生産性ははるかに優れています。それはあなたの時間の大部分を浪費し、あなたのプログラミング目標からあなたをやる気にさせる/気を散らすでしょう。

編集:手続き間最適化リンク時間最適化、およびプログラム全体の最適化も参照)は、「ユニティビルド」の最適化の利点を達成しようとします。これの実装は、ほとんどのコンパイラAFAIKでまだ少し不安定ですが、最終的にはパフォーマンス上の利点を克服する可能性があります。

于 2012-09-12T02:14:30.837 に答える
4

これがRealzの言ったこととあまり似ていないことを願っています。

実行可能(/オブジェクト)サイズ:(実行可能0%/ヘッダーのみで最大50%大きいオブジェクト)

ヘッダーファイルで定義された関数がすべてのオブジェクトにコピーされると思います。実行可能ファイルの生成に関しては、重複する関数を切り取るのはかなり簡単なはずです(どのリンカーがこれを行うかどうかはわかりませんが、ほとんどの場合はそうだと思います)。したがって、(おそらく)実際の違いはありません。実行可能サイズですが、オブジェクトサイズでも十分です。違いは、プロジェクトの残りの部分と実際にヘッダーにあるコードの量に大きく依存するはずです。リンク時間を除いて、オブジェクトのサイズが最近本当に重要であるというわけではありません。

ランタイム:(1%)

インライン関数を除いて、基本的に同じです(関数アドレスは関数アドレスです)。インライン関数は、平均的なプログラムで1%未満の違いしか生じないと思います。これは、関数呼び出しにはある程度のオーバーヘッドがありますが、これは、プログラムで実際に何かを行うオーバーヘッドと比較して何もありません。

メモリフットプリント:(0%)

リンカが重複する関数を切り取ると仮定すると、実行可能ファイル内の同じもの=同じメモリフットプリント(実行時)。重複する機能が切り取られていない場合、それはかなりの違いを生む可能性があります。

コンパイル時間(プロジェクト全体と1つのファイルの変更の両方):(どちらか一方の場合は全体で最大50%速く、ヘッダーのみではなく単一の場合は最大99%速くなります)

大きな違い。ヘッダーファイル内の何かを変更すると、それを含むすべてが再コンパイルされますが、cppファイル内の変更では、そのオブジェクトを再作成して再リンクする必要があります。また、ヘッダーのみのライブラリの完全コンパイルでは、簡単に50%遅くなります。ただし、ヘッダーのプリコンパイルまたはユニティビルドを使用すると、ヘッダーのみのライブラリを使用した完全コンパイルの方が高速になる可能性がありますが、再コンパイルに多くのファイルを必要とする1つの変更は大きな欠点であり、それだけの価値はありません。 。完全な再コンパイルは頻繁には必要ありません。また、cppファイルにはインクルードできますが、ヘッダーファイルにはインクルードできません(これは頻繁に発生する可能性があります)。したがって、適切に設計されたプログラム(ツリーのような依存関係構造/モジュール性)では、関数宣言などを変更するときに(常に必要です)ヘッダーファイルへの変更)、

リンク時間:(ヘッダーのみの場合は最大50%高速)

オブジェクトはおそらく大きいので、処理に時間がかかります。おそらく、ファイルの大きさに比例します。大きなプロジェクト(コンパイル+リンク時間が実際に問題になるほど長い)での私の限られた経験から、リンク時間はコンパイル時間と比較してほとんど無視できます(小さな変更とビルドを続けない限り、あなたはそれを感じると思います、これは頻繁に発生する可能性があると思います)。

于 2012-10-02T15:31:42.693 に答える