すべてをインラインでマークしてみませんか?
-
10-10-2019 - |
質問
まず、私はそうです いいえ コンパイラにすべての関数の実装をインラインにするように強制する方法を探しています。
見当違いの答えのレベルを下げるために inline
キーワードは実際には意味があります。ここに良い説明があります、 インラインvs静的対外部.
だから私の質問、なぜすべての関数定義をマークしないのか inline
?つまり、理想的には、唯一の編集ユニットはそうです main.cpp
. 。または、ヘッダーファイル(PIMPL IDIOMなど)で定義できない関数については、さらにいくつか。
この奇妙なリクエストの背後にある理論は、オプティマイザーに操作する最大情報を提供するということです。もちろん、機能の実装をインラインにすることもできますが、モジュールが1つしかないため、「クロスモジュール」最適化も行う可能性があります。他の利点はありますか?
実際のアプリケーションでこれを試した人はいますか?パフォーマンスは増加しましたか?下降?!?
すべての関数定義をマークすることの欠点は何ですか inline
?
- 編集は遅くなる可能性があり、より多くのメモリを消費します。
- 反復ビルドが壊れており、アプリケーション全体を変更するたびに再構築する必要があります。
- リンク時間は天文学的かもしれません
これらの欠点はすべて、開発者にのみ影響します。ランタイムの欠点は何ですか?
解決
本当に意味がありましたか #include
すべての?これにより、単一のモジュールのみが提供され、オプティマイザーにプログラム全体を一度に表示できます。
実際、MicrosoftのVisualC ++は、 /GL
(プログラム全体の最適化)スイッチ, 、リンカーが実行され、すべてのコードにアクセスできるまで、実際には何もコンパイルしません。他のコンパイラにも同様のオプションがあります。
他のヒント
SQLiteはこのアイデアを使用します。開発中は、従来のソース構造を使用します。ただし、実際の使用には、1つの巨大なCファイル(112K行)があります。最適化のためにこれを行います。約5〜10%のパフォーマンス改善を主張します
私たち(および他のいくつかのゲーム会社)は、1つのuber -.cppを作成して試してみました。 #include
他のすべてのエド。それは既知のテクニックです。私たちの場合、それはランタイムにあまり影響を与えていなかったようですが、あなたが言及したコンパイル時間の欠点は完全に不自由であることが判明しました。 1回の変更ごとに30分のコンパイルを使用すると、効果的に反復することが不可能になります。 (これは、アプリが12を超える異なるライブラリに分割されていることにあります。)
デバッグ中に複数の.OBJを使用し、リリース-OPTビルドでのみUber-CPPを使用するように、別の構成を作成しようとしましたが、その後、単にメモリが不足しているコンパイラの問題に遭遇しました。十分に大きなアプリの場合、ツールは数百万回のラインCPPファイルをコンパイルするだけではありません。
LTCGも試してみましたが、リンクフェーズで単純にクラッシュしなかったまれな場合には、小規模だが良いランタイムブーストが提供されました。
興味深い質問!リストされている欠点のすべてが開発者に固有のものであることは確かに正しいです。ただし、恵まれない開発者が高品質の製品を生産する可能性がはるかに低いことをお勧めします。ランタイムの欠点はないかもしれませんが、各コンパイルが完了するのに数時間(または数日)かかる場合、開発者が小さな変更を加えることを嫌がることを想像してください。
これを「時期尚早の最適化」角度から見ていきます。複数のファイルのモジュラーコードにより、プログラマーの寿命が容易になるため、このように物事を行うことには明らかな利点があります。特定のアプリケーションが遅すぎることが判明し、すべてを挿入することが測定された改善をもたらすことが示される場合のみ、私も 検討 開発者にご不便をおかけします。それでも、開発の大部分が実行された後(測定できるように)、おそらく生産ビルドのみで行われるでしょう。
これは半関連ですが、Visual C ++にはモジュール全体のインラインを含むクロスモジュールの最適化を行う機能があることに注意してください。見る http://msdn.microsoft.com/en-us/library/0zza0de8%28vs.80%29.aspx 詳細。
元の質問に答えを追加するために、オプティマイザーが十分にスマートであると仮定して、実行時に欠点があるとは思わない(したがって、それがVisual Studioで最適化オプションとして追加された理由)。あなたが言及したすべての問題を作成することなく、自動的にそれを行うのに十分なほどスマートなコンパイラを使用するだけです。 :)
少しのメリットモダンなプラットフォームの良いコンパイラで、 inline
非常に少数の関数のみに影響します。それはただです ヒント コンパイラにとって、Modern Compilerはこの決定を自分で行うのがかなり得意であり、関数呼び出しのオーバーヘッドはかなり小さくなりました(多くの場合、インラインの主な利点は、コールオーバーヘッドを減らすことではなく、さらに最適化を開始することです)。
時間をまとめます ただし、インラインもセマンティクスを変更するため、 #include
すべてが1つの巨大なコンパイルユニットに入ります。これ いつもの コンパイル時間を大幅に増やします。これは、大規模なプロジェクトで殺人者です。
コードサイズ
現在のデスクトッププラットフォームとその高性能コンパイラーから離れると、事態は大きく変わります。この場合、巧妙でないコンパイラによって生成されるコードサイズの増加は問題になります - コードが大幅に遅くなります。組み込みプラットフォームでは、通常、コードサイズが最初の制限です。
それでも、一部のプロジェクトは「すべてのインライン」から利益を得ることができます。少なくともあなたのコンパイラが盲目的に従わない場合、それはあなたにリンク時間の最適化と同じ効果を与えます inline
.
場合によってはすでに行われています。それはのアイデアに非常に似ています Unity Builds, 、そして、利点と短所はあなたがデスチベであるものからFAではありません:
- コンパイラが最適化する可能性があります
- リンク時間は基本的に消えます(すべてが単一の翻訳ユニットにある場合、リンクするものは何もありません)
- コンパイル時間は、まあ、何らかの方法で進みます。あなたが言ったように、増分ビルドは不可能になります。一方、完全なビルドはそれ以外の場合よりも高速になります(コードのすべての行が正確に1回コンパイルされるため、通常のビルドでは、ヘッダーのヘッダーのコードがヘッダーが含まれているすべての翻訳ユニットにコンパイルされます。 ))
しかし、すでに多くのヘッダーのみのコードを持っている場合(たとえば、多くのブーストを使用する場合)、ビルド時間と実行可能なパフォーマンスの両方の点で、非常に価値のある最適化かもしれません。
いつものように、パフォーマンスが関係する場合、それは依存します。悪い考えではありませんが、普遍的に適用されるわけでもありません。
膨大な時間に関する限り、基本的に最適化する2つの方法があります。
- 翻訳ユニットの数を最小限に抑える(ヘッダーが少ない場所に含まれるため)、または
- ヘッダー内のコードの量を最小化するため(複数の翻訳ユニットにヘッダーを含めるコストが減少します)
cコードは通常、2番目のオプションを取ります。ほぼ極端になります。フォワード宣言とマクロがヘッダーに保持されることはほとんどありません。 C ++はしばしば真ん中にあります。これは、可能な最悪の総ビルド時間を取得する場所です(ただし、PCHおよび/または増分ビルドは再び時間を削減する可能性があります)が、さらに他の方向に進み、翻訳ユニットの数を最小限に抑えることができます。総ビルド時間に対して本当に驚異的です。
それはほとんど背後にある哲学です プログラム全体の最適化 リンク時間コード生成(LTCG):最適化の機会は、グローバルな知識に最適です。
実用的な観点から、それは一種の痛みのようなものです。なぜなら、あなたが行ったすべての変更には、ソースツリー全体の再コンパイルが必要になるからです。一般的に言えば、任意の変更を加えるために必要なよりも少ない頻度で最適化されたビルドが必要です。
Metrowerksの時代にこれを試しました(「Unity」スタイルのビルドでセットアップするのは非常に簡単です)。コンピレーションは終了しませんでした。私はそれが彼らが予想していなかった方法でツールチェーンに課税する可能性が高いワークフローのセットアップであることを指摘するためだけに言及します。
ここでの仮定は、コンパイラが関数間で最適化できないということです。それは特定のコンパイラの制限であり、一般的な問題ではありません。これを特定の問題の一般的なソリューションとして使用するのは悪いかもしれません。コンパイラは、他の場所でコンパイルされている同じメモリアドレス(キャッシュを使用する)で再利用可能な機能であった可能性がある(およびキャッシュのためにパフォーマンスを失う)ことで、プログラムを非常によく膨らませることができます。
一般的な関数一般的なコスト最適化では、ローカル変数のオーバーヘッドと関数のコードの量との間にバランスがあります。関数内の変数の数をプラットフォームの使い捨て変数の数に保つ(両方とも渡され、ローカル、グローバル)を維持すると、ほとんどのすべてがレジスタにとどまることができ、RAMに排除する必要もありません。フレームは必要ありません(ターゲットに依存します)ため、オーバーヘッドを呼び出す関数は著しく減少します。現実世界のアプリケーションでは常にやるのは難しいですが、代替案は、多くのローカル変数を備えた少数の大きな関数です。目標)。
機能だけでなく、プログラム全体で最適化できるLLVMを試してください。リリース27は、少なくとも1つか2つのテストでGCCのオプティマイザーに追いついていましたが、私は徹底的なパフォーマンステストを行いませんでした。そして28が出ているので、私はそれがより良いと思います。いくつかのファイルであっても、チューニングノブの組み合わせの数は、混乱するには多すぎます。プログラム全体を1つのファイルにするまで最適化しないことが最善であり、最適化を実行し、基本的にはインラインでやろうとしていることをオプティマイザーに連絡しますが、手荷物はありません。
仮定する foo()
と bar()
どちらもいくつかと呼びます helper()
. 。すべてが1つのコンパイルユニットにある場合、コンパイラはインラインではないことを選択する場合があります helper()
, 、総命令サイズを縮小するため。これは〜をひき起こす foo()
非インライラの関数を呼び出すには helper()
.
コンパイラは、のナノ秒の改善が実行時間を改善することを知りません foo()
予想のために、1日あたり100ドルを収益に追加します。パフォーマンスの改善や外部のものの劣化がわかりません foo()
収益に影響はありません。
プログラマーとしてのあなただけがこれらのことを知っています(もちろん、慎重なプロファイリングと分析の後)。インラインではない決定 bar()
コンパイラにあなたが知っていることを伝える方法です。
インランスの問題は、高性能関数がキャッシュに適合することを望んでいることです。関数コールオーバーヘッドは大きなパフォーマンスのヒットだと思うかもしれませんが、多くのアーキテクチャでは、キャッシュミスがカップルのプッシュとポップを水から吹き飛ばすでしょう。たとえば、メインの高性能パスから非常にまれに呼ばれる必要がある大きな(おそらく深い)関数がある場合、メインの高性能ループがL1 Icacheに収まらないポイントに成長する可能性があります。これにより、時折の関数呼び出しよりもはるかに多くのコードが遅くなります。