-1

rdtsc で関数呼び出しの時間を測定したい。そこで、以下の2つの方法で測定しました。

  1. ループで呼び出します。ループ内の各 rdtsc 差を集計し、呼び出し数で割ります。(これをNとしましょう)
  2. ループで呼び出します。ループ自体の rdtsc 差を取得し、N で割ります。

しかし、一貫性のない動作がいくつか見られます。

  1. N を増やすと、方法 1 と 2 の両方で時間がかなり単調に減少します。方法 2 の場合、ループ制御のオーバーヘッドが償却されるという点で理解できます。しかし、方法1の場合はどうなるかわかりません。
  2. 実際、方法 2 では、N を増やすたびに、N=1 の値が毎回新しい N で割られているように見えます。gdb の逆アセンブルを調べると、2 番目のケースでループがスキップされる -O2 でのコンパイラの最適化であることがわかりました。そこで、-O0 を使用して再試行しました。ここで、gdb 逆アセンブリは、2 番目のケースでも実際のループが存在することを示しています。

コードを以下に示します。

    #include <stdio.h>
    #include <inttypes.h>
    #include <stdlib.h>

    typedef unsigned long long ticks;

    static __inline__ ticks getticks(void) {
      unsigned a, d; 
      asm volatile("rdtsc" : "=a" (a), "=d" (d)); 
      return ((ticks)a) | (((ticks)d) << 32); 
    }

    __attribute__ ((noinline))
    void bar() {

    }

    int main(int argc, char** argv) {

       long long N = 1000000; 
       N = atoi(argv[1]);
       int i;
       long long bar_total = 0;

       ticks start = 0, end = 0;

       for (i = 0; i < N; i++) {
         start = getticks();
         bar();
         end = getticks();
         bar_total += (end - start);
       } 

       fprintf(stdout, "Total invocations : %lld\n", N);
       fprintf(stdout, "[regular] bar overhead : %lf\n", ((double)bar_total/  N));

      start = getticks();
      for (i = 0; i < N; i++) {
        bar();
      } 
      end = getticks();

      bar_total = (end - start);

      fprintf(stdout, "[Loop] bar overhead : %lf\n", ((double)bar_total/ N));

      return 0;

     }

ここで何が起こっているのか分かりますか?必要に応じて、gdb 逆アセンブリも配置できます。http://dasher.wustl.edu/tinker/distribution/fftw/kernel/cycle.hの rdtsc 実装を使用しました

編集: 2 番目のケースでは、-O0 で時間が N に正比例して減少するという 2 番目のステートメントを撤回する必要があります。ビルド中に犯した間違いが原因で、古いバージョンが残っていると思います。どのようにしても、方法 1 の図に沿っていくらか低下します。異なる N 値の数値を次に示します。

taskset -c 2 ./example.exe 1
Total invocations : 1
[regular] bar overhead : 108.000000
[Loop] bar overhead : 138.000000

taskset -c 2 ./example.exe 10
Total invocations : 10
[regular] bar overhead : 52.900000
[Loop] bar overhead : 40.700000

taskset -c 2 ./example.exe 100
Total invocations : 100
[regular] bar overhead : 46.780000
[Loop] bar overhead : 15.570000

taskset -c 2 ./example.exe 1000
Total invocations : 1000
[regular] bar overhead : 46.069000
[Loop] bar overhead : 13.669000

taskset -c 2 ./example.exe 100000
Total invocations : 10000
[regular] bar overhead : 46.010100
[Loop] bar overhead : 13.444900

taskset -c 2 ./example.exe 100000000
Total invocations : 100000000
[regular] bar overhead : 26.970272
[Loop] bar overhead : 5.201252

taskset -c 2 ./example.exe 1000000000
Total invocations : 1000000000
[regular] bar overhead : 18.853279
[Loop] bar overhead : 5.218234

taskset -c 2 ./example.exe 10000000000
Total invocations : 1410065408
[regular] bar overhead : 18.540719
[Loop] bar overhead : 5.216395

現在、2 つの新しい動作が見られます。

  1. 方法 1 は、方法 2 よりも収束が遅くなります。しかし、異なる N 設定の値になぜこのような劇的な違いがあるのか​​ について、私は困惑しています。おそらく、現時点では見られないいくつかの基本的な間違いをここで行っています。
  2. 方法 1 の値は、実際には方法 2 よりもいくらか大きくなっています。ループ制御のオーバーヘッドが含まれていないため、方法 2 の値と同等かそれよりわずかに小さいと予想しました。

質問

要約すると、私の質問は

  1. Nを増やすと、両方の方法で得られる値が大幅に変化するのはなぜですか? 特に、ループ制御のオーバーヘッドを考慮しない方法 1 の場合。

  2. 最初の方法が計算でループ制御のオーバーヘッドを除外しているのに、2 番目の方法の結果が最初の方法の結果よりも小さいのはなぜですか?

編集 2

推奨されるrdtscpソリューションについて。

インラインアセンブリに慣れていないので、次のことを行いました。

static __inline__ ticks getstart(void) {
  unsigned cycles_high = 0, cycles_low = 0; 
  asm volatile ("CPUID\n\t"
             "RDTSC\n\t"
             "mov %%edx, %0\n\t"
             "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
             "%rax", "%rbx", "%rcx", "%rdx");
  return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
}

static __inline__ ticks getend(void) {
  unsigned cycles_high = 0, cycles_low = 0; 
  asm volatile("RDTSCP\n\t"
         "mov %%edx, %0\n\t"
          "mov %%eax, %1\n\t"
           "CPUID\n\t": "=r" (cycles_high), "=r" (cycles_low)::
           "%rax", "%rbx", "%rcx", "%rdx");
  return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
}

関数呼び出しの前後に上記のメソッドを使用します。しかし、今では次のような無意味な結果が得られます。

Total invocations : 1000000
[regular] bar overhead : 304743228324.708374
[Loop] bar overhead : 33145641307.734016

キャッチは何ですか?複数の場所で使用されているため、それらをインラインメソッドとして除外したかったのです。

A. コメントの解決策。

4

2 に答える 2

2

rdtscXeon や Core などの順不同の CPU では正しく動作しない可能性がある単純な命令を使用します。シリアル化命令を追加するか、命令に切り替える必要がありrdtscpます

http://en.wikipedia.org/wiki/Time_Stamp_Counter

Pentium Pro 以降、Intel プロセッサはアウトオブオーダー実行をサポートしており、実行可能ファイルに表示される順序で命令が実行されるとは限りません。これにより、RDTSC が予想よりも遅く実行され、誤解を招くサイクル カウントが生成される可能性があります。[3] この問題は、CPUID などのシリアル化命令を実行して、先行するすべての命令を強制的に完了させてからプログラムを続行させるか、または RDTSC 命令のシリアル化バリアントである RDTSCP 命令を使用することによって解決できます。

Intel は rdtsc/rdtscp の使用に関する最近のマニュアルを持っています - How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures (ia-32-ia-64-benchmark-code-execution-paper.pdf, 324264-001) 、2010)。彼らは、開始タイマーには cpuid+rdtsc を、終了タイマーには rdtscp を推奨しています。

RDTPSCPセクション 0 で示した問題の解決策は、と の 2 つの命令の直後に CPUID 命令を追加することですmov(メモリに と の値を格納するためedx) eax。実装は次のとおりです。

asm volatile ("CPUID\n\t"
 "RDTSC\n\t"
 "mov %%edx, %0\n\t"
 "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low)::
"%rax", "%rbx", "%rcx", "%rdx");
/***********************************/
/*call the function to measure here*/
/***********************************/
asm volatile("RDTSCP\n\t"
 "mov %%edx, %0\n\t"
 "mov %%eax, %1\n\t"
 "CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1)::
"%rax", "%rbx", "%rcx", "%rdx");

start = ( ((uint64_t)cycles_high << 32) | cycles_low );
end = ( ((uint64_t)cycles_high1 << 32) | cycles_low1 );

上記のコードでは、最初のCPUID呼び出しはバリアを実装して、命令の上下の命令の順不同の実行を回避しRDTSCます。RDTSCただし、この呼び出しは、タイムスタンプ レジスタが読み取られる前に行われるため、測定には影響しません 。1 つ目RDTSCは、タイムスタンプ レジスタを読み取り、値がメモリに格納されます。次に、測定したいコードが実行されます。コードが関数の呼び出しである場合は、そのような関数を "<code>inline" として宣言することをお勧めします。これにより、アセンブリの観点から、関数自体を呼び出す際のオーバーヘッドがなくなります。このRDTSCP命令はタイムスタンプ レジスタを 2 回読み取り、測定したいすべてのコードの実行が完了したことを保証します。

あなたの例はあまり正しくありません。空の関数を測定しようとしましbar()たが、非常に短いため、方法 1 で rdtsc オーバーヘッドを測定しています ( for() { rdtsc; bar(); rdtsc))。アグナー・フォグの haswell の表によると - http://www.agner.org/optimize/instruction_tables.pdf 191 ページ (長い表「インテル Haswell 命令タイミングとμop 内訳のリスト」、その最後) RDTSCには 15 があります。 uops (フュージョン不可) と 24 ティックのレイテンシ。RDTSCP(古いマイクロアーキテクチャの場合、Sandy Bridge のレイテンシは 23 uops と 36 ticks であるのに対し、rdtsc では 21 uops と 28 ticks です)。したがって、プレーンな rdtsc (または rdtscp) を使用して、そのような短いコードを直接測定することはできません。

于 2015-03-22T01:51:05.117 に答える
1

試しましたclock_gettime(CLOCK_MONOTONIC, &tp)か?サイクルカウンターを手動で読み取るのにかなり近いはずですが、サイクルカウンターがCPUコア間で同期されていない可能性があることにも注意してください。

于 2015-03-22T01:51:29.120 に答える