考慮する:
#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;
const int times = 1000;
const int N = 100000;
void run() {
for (int j = 0; j < N; j++) {
}
}
int main() {
clock_t main_start = clock();
for (int i = 0; i < times; i++) {
clock_t start = clock();
run();
cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
//usleep(1000);
}
cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}
以下にサンプル コードを示します。タイミング ループの最初の 26 回の反復では、run
関数のコストは約 0.4 ミリ秒ですが、その後コストは 0.2 ミリ秒に減少します。
のコメントが解除されるとusleep
、遅延ループはすべての実行で 0.4 ミリ秒かかり、速度が上がることはありません。なぜでしょうか?
コードはg++ -O0
(最適化なし)でコンパイルされているため、遅延ループは最適化されません。Intel(R) Core(TM) で実行されます。i3-3220CPU @ 3.30 GHz、3.13.0-32-genericウブントゥ 14.04.1LTS (信頼できるタール)。
ベストアンサー1
26回の繰り返しの後、LinuxはプロセスがフルにCPUを使用するので、CPUを最大クロック速度まで上げます。タイムスライス何回か続けて。
実時間ではなくパフォーマンスカウンタで確認すると、遅延ループあたりのコアクロックサイクルは一定のままであり、これは単にドブフ(最近の CPU はすべて、ほとんどの場合、よりエネルギー効率の高い周波数と電圧で動作するためにこれを使用します)。
テストした場合スカイレイクカーネルサポート付き新しい電源管理モード(ハードウェアがクロック速度を完全に制御する)、立ち上げははるかに早く起こるでしょう。
しばらく放置しておくとターボ付きIntel CPU熱制限によりクロック速度が最大持続周波数まで低下すると、反復あたりの時間が再びわずかに増加する可能性があります。(なぜCPUはHPCで最高のパフォーマンスを維持できないのか高電力ワークロードに対応できる速度よりも CPU を高速に実行できる Turbo の詳細については、こちらをご覧ください。
ご紹介usleep
防止するLinux の CPU 周波数調整器プロセスは最小周波数でも 100% の負荷を生成しないため、クロック速度を上げることはできません。(つまり、カーネルのヒューリスティックにより、CPU は実行中のワークロードに対して十分な速度で実行されていると判断されます。)
他の理論に関するコメント:
返信:usleep
潜在的なコンテキストスイッチがキャッシュを汚染する可能性があるというデビッドの理論: 一般的には悪い考えではありませんが、このコードの説明には役立ちません。
この実験ではキャッシュ/TLB汚染は全く重要ではないタイミング ウィンドウ内では、スタックの最後以外、メモリにアクセスするものは基本的に何もありません。ほとんどの時間は、int
スタック メモリの 1 つだけにアクセスする小さなループ (命令キャッシュの 1 行) で費やされます。このコードの実行中に発生する可能性のあるキャッシュ汚染は、usleep
時間のごく一部にすぎません (実際のコードは異なります)。
x86 の場合の詳細:
自身の呼び出しはclock()
キャッシュ ミスになる可能性がありますが、コード フェッチ キャッシュ ミスは、測定対象の一部となるのではなく、開始時間の測定を遅らせます。 の 2 番目の呼び出しは、clock()
キャッシュ内でまだホットであるため、遅延することはほとんどありません。
関数は、 (gccが「コールド」としてマークするため、最適化が弱まり、他のコールド関数/データと一緒に配置されるため)run
異なるキャッシュラインにある可能性があります。1つまたは2つのmain
main
命令キャッシュミスただし、おそらく同じ 4k ページ内にあるため、main
プログラムの時間指定領域に入る前に潜在的な TLB ミスがトリガーされることになります。
gcc -O0はOPのコードをコンパイルしてこのようなもの(Godbolt Compiler エクスプローラー): ループ カウンターをスタック上のメモリに保持します。
空のループはループカウンタをスタックメモリに保持するため、典型的なインテル x86 CPUループは、add
メモリの宛先 (読み取り、変更、書き込み) の一部であるストア フォワーディング レイテンシのおかげで、OP の IvyBridge CPU 上で約 6 サイクルごとに 1 回の反復で実行されます。100k iterations * 6 cycles/iteration
は 60 万サイクルで、最大で 2 回のキャッシュ ミス (解決されるまでそれ以上の命令の発行を妨げるコード フェッチ ミスごとに約 200 サイクル) の影響が支配的です。
アウトオブオーダー実行とストアフォワーディングにより、スタックへのアクセス時(call
命令の一部として)の潜在的なキャッシュミスがほとんど隠されるはずです。
ループカウンタがレジスタ内に保持されていたとしても、10 万サイクルは多すぎます。