なぜリンカの最適化がそんなに悪いのですか?
-
05-07-2019 - |
質問
最近、同僚が私に、すべてを単一のファイルにコンパイルすると、個別のオブジェクトファイルをコンパイルするよりもはるかに効率的なコードが作成されることを指摘しました。また、プロジェクトの合計コンパイル時間が大幅に短縮されました。 C ++を使用する主な理由の1つがコード効率であることを考えると、これは私にとって驚くべきことでした。
明らかに、アーカイバ/リンカーがオブジェクトファイルからライブラリを作成したり、それらを実行可能ファイルにリンクすると、単純な最適化でもペナルティが科せられます。次の例では、コンパイラの代わりにリンカで実行すると、些細なインライン化のパフォーマンスが1.8%低下します。コンパイラテクノロジは、このようなかなり一般的な状況を処理するのに十分に高度であるように見えますが、実際には発生していません。
Visual Studio 2008を使用した簡単な例を次に示します。
#include <cstdlib>
#include <iostream>
#include <boost/timer.hpp>
using namespace std;
int foo(int x);
int foo2(int x) { return x++; }
int main(int argc, char** argv)
{
boost::timer t;
t.restart();
for (int i=0; i<atoi(argv[1]); i++)
foo (i);
cout << "time : " << t.elapsed() << endl;
t.restart();
for (int i=0; i<atoi(argv[1]); i++)
foo2 (i);
cout << "time : " << t.elapsed() << endl;
}
foo.cpp
int foo (int x) { return x++; }
実行の結果:インライン foo2
の代わりにリンクされた foo
を使用すると、パフォーマンスが1.8%低下します。
$ ./release/testlink.exe 100000000
time : 13.375
time : 13.14
はい、リンカー最適化フラグ(/ LTCG)はオンです。
解決
私はコンパイラの専門家ではありませんが、コンパイラは言語ツリーで動作するため、オブジェクト出力を操作するためにコンテンツ自体を処理する必要があるリンカとは対照的に、最適化するために自由に利用できる情報がはるかに多いと思いますコンパイラが見たコードよりはるかに表現力が劣ります。したがって、リンカーとコンパイラーの開発チームは、理論的にはコンパイラーが行うトリックに一致する可能性のあるリンカー最適化を行うための労力が少なくなります。
ところで、申し訳ありませんが、ltcgの議論にあなたの元の質問をそらしてしまいました。あなたの質問は少し異なっていて、リンク時間とコンパイル時間の静的最適化の可能性/利用可能性に関心があります。
他のヒント
同僚が古くなっています。この技術は2003年以降(MS C ++コンパイラ上)にあります: / LTCG 。リンク時のコード生成は、まさにこの問題に対処しています。私が知っていることから、 GCC には次世代コンパイラのレーダーにこの機能があります。
LTCGは、モジュール間で関数をインライン化するなどのコードを最適化するだけでなく、実際にコードを再調整してキャッシュの局所性と特定の負荷の分岐を最適化します。プロファイルに基づく最適化。これらのオプションは、ビルドが完了するまでに数時間かかる場合があるため、リリースビルド専用です。通常、インストルメント済み実行可能ファイルをリンクし、プロファイリングロードを実行してから、プロファイリング結果と再度リンクします。リンクには、LTCGで何が最適化されているかについての詳細が含まれています。
インライン&#8211;たとえば、 頻繁に機能Aが存在する 関数Bを呼び出し、関数Bは 比較的小さく、プロファイルガイド付き 最適化は関数Bをインライン化します 関数Aで。
仮想コール投機&#8211;もし 仮想通話、または他の通話 関数ポインタ、頻繁にターゲット 特定の機能、プロファイルガイド 最適化は挿入できます 条件付きで実行される直接呼び出し 頻繁にターゲットとされる機能、および 直接呼び出しはインライン化できます。
割り当てを登録&#8211;最適化 プロファイルデータはより良い結果をもたらします レジスタ割り当て。
基本ブロックの最適化&#8211;基本ブロック 最適化により、一般的に実行できます 一時的に実行する基本ブロック 所定のフレーム内に配置される 同じページのセット(ローカリティ)。この 使用するページ数を最小限に抑える したがって、メモリのオーバーヘッドを最小限に抑えます。
サイズ/速度の最適化&#8211;関数 プログラムが多くの時間を費やす場所 速度を最適化できます。
関数のレイアウト&#8211;呼び出しに基づいて グラフとプロファイルされた呼び出し元/呼び出し先 行動、傾向がある機能 同じ実行パスに沿って 同じセクションに配置されます。
条件付きブランチ最適化&#8211;と プロファイルにガイドされた値プローブ 最適化は、特定の switchステートメントの値が使用されます 他の値よりも頻繁に。この その後、値を引き出すことができます switchステートメント。同じことができます if / else命令を使用して、 オプティマイザーはif / elseを注文できます ifまたはelseブロックが どのブロックに応じて最初に配置されます より頻繁に当てはまります。
デッドコード分離&#8211;あるコード プロファイリング中に呼び出されない場合は移動されます 追加される特別なセクションへ セクションのセットの最後まで。 これにより、このセクションが効果的に保持されます 頻繁に使用されるページのうち。
EHコード分離&#8211; EHコード、 例外的に実行されていることができます 多くの場合、別のセクションに移動します プロファイルに基づく最適化が可能な場合 例外が発生することを決定する 例外的な条件でのみ。
メモリ組み込み関数&#8211;の拡大 組み込み関数は、 組み込み関数が 頻繁に呼び出されます。組み込み缶 また、ブロックに基づいて最適化される 移動またはコピーのサイズ。
あなたの同僚は私たちのほとんどより賢いです。最初は粗雑なアプローチに見えたとしても、単一の.cppファイルへのプロジェクトのインライン化には、リンク時最適化などの他のアプローチにはないものと、しばらくはないものがあります-信頼性
しかし、あなたはこれを2年前に尋ねました、そしてそれから多くが変わったと証言します(少なくともg ++では)。たとえば、仮想化ははるかに信頼性が高くなります。