web-dev-qa-db-ja.com

スリープせずに数回繰り返した後、この遅延ループの実行が速くなるのはなぜですか?

考慮してください:

#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µmsかかり、速度は上がりません。どうして?

コードはg++ -O0(最適化なし)でコンパイルされているため、遅延ループは最適化されません。 Intel(R)Core(TM) i3-322 CPU @ 3.30 GHz、3.13.0-32-genericで実行 buntu 14.04.1 LTS(Trusty Tahr) 。

71
phyxnj

26回の反復の後、Linuxはプロセスがその完全な タイムスライス を数回連続して使用するため、CPUを最大クロック速度まで上昇させます。

壁時計時間の代わりにパフォーマンスカウンターでチェックした場合、遅延ループごとのコアクロックサイクルが一定のままであることがわかり、それが [〜#〜] dvfs [〜#〜 ] (ほとんどすべての最新のCPUは、よりエネルギー効率の高い周波数と電圧で実行するために使用します)。

Skylake でテストした場合、カーネルは 新しい電源管理モード(ハー​​ドウェアがクロック速度を完全に制御する) をサポートし、ランプアップははるかに速くなります。

Turbo搭載のIntel CP でしばらく実行したままにしておくと、温度制限により最大持続周波数まで低下させるためにクロック速度が必要になると、反復あたりの時間が再びわずかに増加する可能性があります。


usleepを導入すると、プロセスが実行されないため、 LinuxのCPU周波数ガバナー がクロック速度を上げない最小周波数でも100%の負荷を生成します。 (つまり、カーネルのヒューリスティックは、CPUが実行されているワークロードに十分な速度で実行されていると判断します。)



他の理論に関するコメント

re: usleepからの潜在的なコンテキストスイッチがキャッシュを汚染する可能性があるというデビッドの理論 :一般的には悪い考えではありませんが、このコードの説明には役立ちません。

この実験では、キャッシュ/ TLB汚染はまったく重要ではありません。タイミングウィンドウ内には、スタックの最後以外のメモリに触れるものは基本的にありません。ほとんどの時間は、スタックメモリの1つのintだけに触れる小さなループ(命令キャッシュの1行)に費やされます。 usleep中の潜在的なキャッシュ汚染は、このコードの時間のほんの一部です(実際のコードは異なります)!

X86の詳細:

clock()への呼び出し自体はキャッシュミスする可能性がありますが、コードフェッチキャッシュミスは、測定対象の一部ではなく、開始時間の測定を遅らせます。 clock()への2番目の呼び出しは、キャッシュ内でまだホットなはずなので、ほとんど遅延しません。

run関数はmainとは異なるキャッシュラインにある場合があります(gccはmainを「コールド」としてマークするため、最適化が少なくなり、他のコールド関数/データと一緒に配置されます) )。 1つまたは2つの instruction-cache misses が期待できます。ただし、おそらく同じ4kページのままなので、mainはプログラムの時間制限領域に入る前に潜在的なTLBミスを引き起こします。

gcc -O0は、OPのコードを このような(Godbolt Compiler Explorer) :スタック上のメモリにループカウンターを保持するようにコンパイルします。

空のループは、ループカウンターをスタックメモリに保持するため、通常の Intel x86 CP OPのIvyBridge CPUでは〜6サイクルごとに1回の反復でループが実行されます。 addのメモリ宛先(読み取り-変更-書き込み)。 100k iterations * 6 cycles/iterationは60万サイクルで、最大2つのキャッシュミスの寄与を支配します(コードフェッチミスごとに最大200サイクルで、解決されるまでさらに命令を発行できません)。

順不同の実行とストア転送は、(call命令の一部として)スタックへのアクセスで潜在的なキャッシュミスをほとんど隠す必要があります。

ループカウンタがレジスタに保持されていたとしても、100kサイクルは大量です。

121
Peter Cordes

usleepを呼び出すと、コンテキストが切り替えられる場合とされない場合があります。存在する場合、そうでない場合よりも時間がかかります。

3
David Schwartz