[テンプレート] C ++プログラムを正しくベンチマークする方法
-
22-07-2019 - |
質問
<!> lt;背景<!> gt;
私は本当にC ++コードを最適化する必要があります。分子シミュレーション用のライブラリを書いていますが、新しい機能を追加する必要があります。過去にこの機能を追加しようとしましたが、ネストループで呼び出される仮想関数を使用しました。私はそれについて悪い感情を抱いており、最初の実装はこれが悪い考えであることを証明しました。ただし、これはコンセプトのテストには問題ありませんでした。
<!> lt; / background <!> gt;
今、この機能を可能な限り高速にする必要があります(アセンブリコードやGPU計算がなくても、これはC ++であり、以下よりも読みやすくなければなりません)。 今、私はテンプレートとクラスポリシーについてもう少し知っています(Alexandrescuの優れた本から)。コンパイル時のコード生成が解決策になると思います。
ただし、ライブラリに実装する巨大な作業を行う前に、デザインをテストする必要があります。問題は、この新機能の効率をテストする最良の方法です。
明らかに、最適化をオンにする必要があります。これがないと、g ++(およびおそらく他のコンパイラも)がオブジェクトコードに不要な操作を保持するからです。また、1e-3秒のデルタが良い設計と悪い設計の違いを生む可能性があるため、ベンチマークで新機能を多用する必要があります(この機能は実際のプログラムでは100万回呼び出されます)。
問題は、g ++が<!> quot; too smart <!> quot;であることです。最適化しながら、計算の結果が決して使用されないと見なす場合、ループ全体を削除できます。出力アセンブリコードを見たときに、それを一度見ました。
標準出力に印刷を追加すると、コンパイラーはループ内で計算を強制されますが、ほとんどの場合、iostream実装のベンチマークを実行します。
では、ライブラリから抽出された小さな機能の正しいベンチマークを実行するにはどうすればよいですか? 関連する質問:この種の in vitro テストを小さな単位で行うのは正しいアプローチですか、それともコンテキスト全体が必要ですか?
アドバイスをありがとう!
微調整を可能にするコンパイラ固有のオプションから、volatile
やextern
などのすべてのコンパイラで動作するより一般的なソリューションまで、いくつかの戦略があるようです。
これらすべてを試してみると思います。 回答ありがとうございます!
解決
any コンパイラに結果を破棄させない場合は、結果をvolatileオブジェクトに書き込みます。その操作は、定義により最適化できません。
template<typename T> void sink(T const& t) {
volatile T sinkhole = t;
}
iostreamのオーバーヘッドはありません。生成されたコードに残さなければならないコピーのみです。 現在、多くの操作から結果を収集している場合、それらを1つずつ廃棄しないことが最善です。これらのコピーは依然としていくらかのオーバーヘッドを追加する可能性があります。代わりに、何らかの方法ですべての結果を単一の不揮発性オブジェクトに収集し(したがって、すべての個別の結果が必要です)、その結果オブジェクトを揮発性に割り当てます。例えば。個々の操作がすべて文字列を生成する場合、1 <!> lt; <!> lt; 32を法としてすべてのchar値を加算することにより、評価を強制できます。これによりオーバーヘッドはほとんど追加されません。文字列はおそらくキャッシュにあります。加算の結果はその後、揮発性に割り当てられるため、各文字列の各文字は実際に計算する必要があり、ショートカットは許可されていません。
他のヒント
アグレッシブなコンパイラ(本当にがある場合)がない限り、チェックサムを計算し(すべての結果を単純に加算する)、チェックサムを出力することをお勧めします。
それ以外では、ベンチマークを実行する前に生成されたアセンブリコードを確認して、ループが実際に実行されていることを視覚的に確認できます。
コンパイラは、発生しないコード分岐を排除することのみが許可されています。ブランチの実行を除外できない限り、ブランチを削除しません。どこかに何らかのデータ依存関係がある限り、コードはそこにあり、実行されます。コンパイラーは、プログラムのどの側面が実行されないかを推定することに関してあまり賢くなく、それを試みません。それはNPの問題であり、ほとんど計算できないからです。 if (0)
などの簡単なチェックがいくつかありますが、それだけです。
私の謙虚な意見は、C / C ++がブール式を評価する方法など、以前に他の問題に遭遇した可能性があるということです。
しかし、とにかく、これは速度のテストに関するものであるため、物事が自分で呼び出されることを確認できます-一度実行してから、もう一度戻り値のテストを実行します。または、増分される静的変数。テストの最後に、生成された数値を印刷します。結果は等しくなります。
生体外試験に関する質問に答えるには:はい、それを行います。アプリが非常にタイムクリティカルである場合、それを行います。一方、説明は別の問題を示唆しています:デルタが1e-3秒の時間枠にある場合、問題のメソッドは非常に頻繁に呼び出される必要があるため、計算の複雑さの問題のように聞こえます(数回の実行、1e-3秒は無視できます)。
モデリングしている問題の領域は非常に複雑で、データセットはおそらく巨大です。そのようなことは常に興味深い努力です。ただし、最初に適切なデータ構造とアルゴリズムを確実に用意し、その後、必要なものをすべて最適化します。 つまり、最初にコンテキスト全体を見てください。;-)
好奇心から、あなたが計算している問題は何ですか?
コンパイルの最適化については、多くの制御があります。 -O1、-O2などは、単なる多数のスイッチのエイリアスです。
マニュアルページから
-O2 turns on all optimization flags specified by -O. It also turns
on the following optimization flags: -fthread-jumps -falign-func‐
tions -falign-jumps -falign-loops -falign-labels -fcaller-saves
-fcrossjumping -fcse-follow-jumps -fcse-skip-blocks
-fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
-fgcse-lm -foptimize-sibling-calls -fpeephole2 -fregmove -fre‐
order-blocks -freorder-functions -frerun-cse-after-loop
-fsched-interblock -fsched-spec -fschedule-insns -fsched‐
ule-insns2 -fstrict-aliasing -fstrict-overflow -ftree-pre
-ftree-vrp
このコマンドを調整して使用すると、調査するオプションを絞り込むことができます。
...
Alternatively you can discover which binary optimizations are
enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts Φ grep enabled
不正行為の最適化が見つかったら、coutを使用する必要はありません。
これが可能であれば、コードを次のように分割してみてください:
- すべての最適化を有効にしてコンパイルしたテスト対象のライブラリ
- 最適化をオフにして、ライブラリを動的にリンクするテストプログラム
それ以外の場合は、optimize属性を使用してテスト関数に異なる最適化レベル(gcc ...を使用しているように見える)を指定できます( http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes ) 。
別のcppファイルでダミー関数を作成できますが、これは何も実行しませんが、計算結果のタイプに関係なく引数として受け取ります。次に、計算の結果を使用してその関数を呼び出し、gccに中間コードを生成させることができます。唯一のペナルティは、関数を呼び出すコストです( lot! )。
#include <iostream>
// Mark coords as extern.
// Compiler is now NOT allowed to optimise away coords
// This it can not remove the loop where you initialise it.
// This is because the code could be used by another compilation unit
extern double coords[500][3];
double coords[500][3];
int main()
{
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
std::cout << "hello world !"<< std::endl;
return 0;
}
編集:実行できる最も簡単な方法は、関数の実行後、ベンチマーク外でデータを誤った方法で使用することです。のように、
StartBenchmarking(); // ie, read a performance counter
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
StopBenchmarking(); // what comes after this won't go into the timer
// this is just to force the compiler to use coords
double foo;
for (int j = 0 ; j < 500 ; ++j )
{
foo += coords[j][0] + coords[j][1] + coords[j][2];
}
cout << foo;
これらの場合に役立つのは、関数内で in vitro テストを非表示にし、ベンチマークデータセットを volatile ポインターに渡すことです。これは、それらのポインターへの後続の書き込みを折りたたんではならないことをコンパイラーに伝えます(これらは、メモリーにマップされたI / Oである可能性があるためです)。だから、
void test1( volatile double *coords )
{
//perform a simple initialization of all coordinates:
for (int i=0; i<1500; i+=3)
{
coords[i+0] = 3.23;
coords[i+1] = 1.345;
coords[i+2] = 123.998;
}
}
何らかの理由で私はまだ理解していませんが、MSVCで常に機能するとは限りませんが、多くの場合は機能します。アセンブリ出力を確認してください。また、 volatile はコンパイラーの最適化を妨げることを覚えておいてください(コンパイラーがポインターの内容をレジスターに保持することを禁止し、プログラムの順序で書き込みを強制的に実行します)。データの最終書き込み。
一般に、このようなin vitroテストは、すべてではないことを覚えている限り、非常に便利です。通常、新しい数学ルーチンをこのように分離してテストし、一貫性のあるデータでアルゴリズムのキャッシュとパイプラインの特性だけをすばやく反復できるようにします。
このような試験管プロファイリングと<!> quot; the real world <!> quot;での実行の違い入力データセットが大きく変化する場合があり(最良の場合、最悪の場合、病理学的な場合もあります)、キャッシュは関数に入ると不明な状態になり、他のスレッドがバスを叩く可能性があります。そのため、完了したら、この関数でいくつかのベンチマークを in vivo で実行する必要があります。
GCCに同様の機能があるかどうかはわかりませんが、VC ++では次を使用できます:
#pragma optimize
選択的に最適化のオン/オフを切り替えます。 GCCに同様の機能がある場合、完全に最適化してビルドし、必要に応じてコードを呼び出してコードを確実に呼び出すことができます。
不要な最適化のほんの一例:
#include <vector>
#include <iostream>
using namespace std;
int main()
{
double coords[500][3];
//perform a simple initialization of all coordinates:
for (int i=0; i<500; ++i)
{
coords[i][0] = 3.23;
coords[i][1] = 1.345;
coords[i][2] = 123.998;
}
cout << "hello world !"<< endl;
return 0;
}
<!> quot; double coords [500] [3] <!> quot;のコードにコメントする場合forループの最後まで、まったく同じアセンブリコードを生成します(g ++ 4.3.2で試しました)。この例は非常に単純すぎて、単純な<!> quot; Coordinates <!> quot;のstd :: vectorを使用してこの動作を示すことができませんでした。構造。
ただし、この例はまだいくつかの最適化によってベンチマークにエラーが発生する可能性があることを示していると思うので、ライブラリに新しいコードを導入するときにこの種の驚きを避けたいと思いました。新しいコンテキストがいくつかの最適化を妨げる可能性があり、非常に非効率的なライブラリにつながる可能性があることは容易に想像できます。
同じことが仮想関数にも当てはまります(ただし、ここでは証明しません)。静的リンクがジョブを実行するコンテキストで使用されます。まともなコンパイラーは、仮想関数の余分な間接呼び出しを排除する必要があると確信しています。この呼び出しをループで試してみて、仮想関数の呼び出しはそれほど大したことではないと結論付けることができます。 次に、コンパイラがポインタの正確なタイプを推測できず、実行時間が20%増加する状況で、数十万回呼び出します。
起動時に、ファイルから読み取ります。コードでは、if(input == <!> quot; x <!> quot;)cout <!> lt; <!> lt; result_of_benchmark;
コンパイラは計算を削除できません。入力が<!> quot; x <!> quot;でないことを確認すると、iostreamのベンチマークは行われません。