19

特定の関数をベンチマークすることにしたので、単純に次のようなコードを書きます。

#include <ctime>
#include <iostream>

int SlowCalculation(int input) { ... }

int main() {
    std::cout << "Benchmark running..." << std::endl;
    std::clock_t start = std::clock();
    int answer = SlowCalculation(42);
    std::clock_t stop = std::clock();
    double delta = (stop - start) * 1.0 / CLOCKS_PER_SEC;
    std::cout << "Benchmark took " << delta << " seconds, and the answer was "
              << answer << '.' << std::endl;
    return 0;
}

同僚は、コードの並べ替えを避けるために変数startstop変数を宣言する必要があると指摘しました。volatile彼は、オプティマイザーが、たとえば、次のようにコードを効果的に並べ替えることができると提案しました。

    std::clock_t start = std::clock();
    std::clock_t stop = std::clock();
    int answer = SlowCalculation(42);

最初は、このような極端な並べ替えが許可されていることに懐疑的でしたが、いくつかの調査と実験の後、許可されていることがわかりました。

しかし、揮発性は適切な解決策とは思えませんでした。本当にメモリマップドI / Oのためだけに揮発性ではありませんか?

それにもかかわらず、私volatileは、ベンチマークにかなり長い時間がかかっただけでなく、実行ごとに非常に一貫性がないことも発見しました. volatile を使用しない場合 (そして幸運にもコードが並べ替えられていないことを確認した場合)、ベンチマークは一貫して 600 ~ 700 ミリ秒かかりました。揮発性では、1200 ミリ秒かかることが多く、5000 ミリ秒以上かかることもありました。2 つのバージョンの逆アセンブル リストには、レジスタの選択が異なること以外、実質的に違いはありませんでした。これは、そのような圧倒的な副作用を持たないコードの並べ替えを回避する別の方法があるかどうか疑問に思います.

私の質問は:

このようなベンチマーク コードでコードの並べ替えを防ぐ最善の方法は何ですか?

私の質問は、これ(並べ替えではなく省略を回避するために volatile を使用することに関するものでした)、これ(並べ替えを防ぐ方法に答えなかった)、およびこれ(問題がコードの並べ替えまたはデッドコードであるかどうかを議論したもの)に似ています排除)。3 つすべてがこの正確なトピックを扱っていますが、実際に私の質問に答えるものはありません。

更新:答えは、私の同僚が間違っていたようで、このような並べ替えは標準と一致していません。私はそう言ったすべての人に賛成票を投じ、賞金をマキシムに授与します.

私が説明したように、Visual Studio 2010 がクロック呼び出しを並べ替えた (この質問のコードに基づく) 1 つのケースを見てきました(64 ビット ビルドのみ)。Microsoft Connect でバグを報告できるように、それを説明する最小限のケースを作成しようとしています。

volatile はメモリへの読み取りと書き込みを強制するため、はるかに遅くする必要があると述べた人にとって、これは発行されるコードと完全に一致していません。この質問に対する私の回答では、volatile を使用する場合と使用しない場合のコードの逆アセンブリを示します。ループ内では、すべてがレジスタに保持されます。唯一の大きな違いは、レジスタの選択のようです。x86 アセンブリを十分に理解していないため、不揮発性バージョンのパフォーマンスが一貫して高速である一方で、揮発性バージョンのパフォーマンスが一貫して(場合によっては劇的に) 遅い理由を知ることができません。

4

8 に答える 8

18

同僚は、コードの並べ替えを避けるために、start 変数と stop 変数を volatile として宣言する必要があると指摘しました。

申し訳ありませんが、あなたの同僚は間違っています。

コンパイラは、コンパイル時に定義を使用できない関数の呼び出しを並べ替えません。コンパイラがそのような呼び出しを並べ替えたり、これらの周りにコードを移動forkしたりした場合に起こるであろう面白さを想像してみてください。exec

つまり、定義のない関数はコンパイル時のメモリ バリアです。つまり、コンパイラは、呼び出しの前に後続のステートメントを移動したり、呼び出しの後に前のステートメントを移動したりしません。

コード呼び出しstd::clockで、定義が利用できない関数を呼び出すことになります。

「 atomic Weapons: The C++ Memory Model and Modern Hardware」volatileは、(コンパイル時の) メモリ バリアやその他多くの有用な事柄についての誤解について説明しているため、十分に視聴することをお勧めしません。

それにもかかわらず、揮発性を追加したところ、ベンチマークが大幅に長くかかるだけでなく、実行ごとに非常に一貫性がないこともわかりました. volatile を使用しない場合 (そして幸運にもコードが並べ替えられていないことを確認した場合)、ベンチマークは一貫して 600 ~ 700 ミリ秒かかりました。揮発性では、1200 ミリ秒かかることが多く、5000 ミリ秒以上かかることもありました

volatileここに責任があるかどうかはわかりません。

報告される実行時間は、ベンチマークの実行方法によって異なります。CPU 周波数スケーリングを無効にして、ターボ モードをオンにしたり、実行中に周波数を切り替えたりしないようにしてください。また、スケジューリングのノイズを避けるために、マイクロベンチマークはリアルタイムの優先度の高いプロセスとして実行する必要があります。別の実行中に、一部のバックグラウンド ファイル インデクサーがベンチマークと CPU 時間の競合を開始する可能性があります。詳しくはこちらをご覧ください。

関数の実行にかかる時間を数回測定し、最小/平均/中央値/最大/標準偏差/合計時間の数値を報告することをお勧めします。高い標準偏差は、上記の準備が行われていないことを示している可能性があります。多くの場合、最初の実行は最も長くなります。これは、CPU キャッシュが冷えている可能性があり、多くのキャッシュ ミスやページ フォールトが発生し、最初の呼び出しで共有ライブラリから動的シンボルが解決される場合があるためです (遅延シンボル解決は、Linux のデフォルトのランタイム リンク モードです)。 、たとえば)、その後の呼び出しははるかに少ないオーバーヘッドで実行されます。

于 2013-02-25T16:35:12.663 に答える
2

並べ替えを防止する通常の方法は、asm volatile ("":::"memory");(gcc を使用して) コンパイル バリアを使用することです。これは何もしない asm 命令ですが、メモリを破壊することをコンパイラに伝えるため、コードを並べ替えることが許可されていません。このコストは、再注文を削除する実際のコストにすぎません。これは、他の場所で提案されているように、最適化レベルなどを変更する場合には明らかに当てはまりません。

_ReadWriteBarrierMicrosoftのものと同等だと思います。

Maxim Yegorushkin の回答によると、並べ替えが問題の原因になる可能性は低いです。

于 2013-02-25T17:10:09.207 に答える
1

C++ でこれを行う正しい方法は、クラスを使用することです。

class Timer
{
    std::clock_t startTime;
    std::clock_t* targetTime;

public:
    Timer(std::clock_t* target) : targetTime(target) { startTime = std::clock(); }
    ~Timer() { *target = std::clock() - startTime; }
};

次のように使用します。

std::clock_t slowTime;
{
    Timer timer(&slowTime);
    int answer = SlowCalculation(42);
}

心に留めておいてください、私はあなたのコンパイラがこのように再注文するとは実際には信じていません。

于 2013-02-25T17:22:11.577 に答える
1

(高レベルの最適化)でSlowCalculationコンパイルされた2つのCファイルと、 (低レベル、まだ最適化されています-そのベンチマーク部分には十分かもしれません)でコンパイルされたベンチマークファイルを作成できます。g++ -O3g++ -O1

man ページによると、コードの並べ替えは-O2および-O3最適化レベルで行われます。

最適化はリンクではなくコンパイル中に行われるため、ベンチマーク側はコードの並べ替えの影響を受けません。

使用していると仮定しますがg++、別のコンパイラには同等のものがあるはずです。

于 2013-02-23T16:29:22.420 に答える
1

私が考えることができるいくつかの方法があります。アイデアは、コンパイラが一連の命令を並べ替えないように、コンパイル時間の障壁を作成することです。

並べ替えを回避する 1 つの可能な方法は、コンパイラによって解決できない命令間の依存関係を強制することです (たとえば、関数へのポインターを渡し、後の命令でそのポインターを使用します)。ベンチマークに関心のある実際のコードのパフォーマンスにどのように影響するかはわかりません。

もう 1 つの可能性は、関数SlowCalculation(42);extern関数にし (この関数を別の .c/.cpp ファイルで定義し、そのファイルをメイン プログラムにリンクする)、startandstopをグローバル変数として宣言することです。コンパイラのリンク時/プロシージャ間オプティマイザによって提供される最適化が何であるかわかりません。

また、O1 または O0 でコンパイルする場合、おそらくコンパイラは命令の並べ替えを気にしません。あなたの質問は、(コンパイル時間の障壁 - コンパイラ コードの並べ替え - gcc と pthreads )に多少関連しています。

于 2013-02-28T02:09:14.123 に答える
1

揮発性が保証することは 1 つだけです。揮発性変数からの読み取りは、毎回メモリから読み取られます。コンパイラは、値がレジスタにキャッシュできるとは想定しません。同様に、書き込みはメモリに書き込まれます。コンパイラは、「メモリに書き出す前に、しばらくの間」レジスタに保持しません。

コンパイラの並べ替えを防ぐために、いわゆるコンパイラ フェンスを使用できます。MSVC には、次の 3 つのコンパイラ フェンスが含まれています。

_ReadWriteBarrier() - フルフェンス

_ReadBarrier() - ロード用の両面フェンス

_WriteBarrier() - 店舗の両面フェンス

ICC には __memory_barrier() のフル フェンスが含まれています。

このレベルではより細かい粒度は必要ないため、通常はフル フェンスが最適な選択です (コンパイラ フェンスは基本的に実行時にコストがかかりません)。

ステートメントの並べ替え (最適化が有効になっている場合にほとんどのコンパイラが行う) は、コンパイラの最適化を使用してコンパイルしたときに特定のプログラムが操作に失敗する主な理由でもあります。

http://preshing.com/20120625/memory-ordering-at-compile-timeを読んで、コンパイラの再順序付けなどで直面する可能性のある潜在的な問題を確認することをお勧めします。

于 2013-02-26T11:06:44.030 に答える
0

あなたの同僚によって説明された並べ替えはちょうど壊れます 1.9/13

前に順序付けされるのは、単一のスレッド (1.10) によって実行される評価間の非対称で推移的なペアワイズ関係であり、これらの評価の間に部分的な順序が生じます。任意の 2 つの評価 A および B が与えられた場合、A が B の前にシーケンスされている場合、A の実行は B の実行よりも優先されます。[ 注: 順序付けされていない評価の実行はオーバーラップする可能性があります。—エンドノート] 評価 A と B は、A が B の前にシーケンスされるか、B が A の前にシーケンスされる場合、不定にシーケンスされますが、どちらが指定されていません。[ 注: 不確定な順序の評価はオーバーラップできませんが、どちらかが最初に実行される可能性があります。—終わりのメモ]

したがって、スレッドを使用していないときは、基本的に並べ替えについて考えるべきではありません。

于 2013-02-25T17:18:12.810 に答える