web-dev-qa-db-ja.com

clock_gettimeがそれほど不安定なのはなぜですか?

イントロ

  • セクション古い質問には最初の質問(さらなる調査結論が含まれています以来追加)。

  • さまざまなタイミング方法(rdtsc、_clock_gettime_、およびQueryThreadCycleTime)の詳細な比較については、以下のセクション詳細調査にスキップしてください。

  • CGTの不安定な動作は、バグのあるカーネルまたはバグのあるCPUのいずれかに起因すると考えられます(セクション結論を参照)。

  • テストに使用されるコードは、この質問の下部にあります(セクション付録を参照)。

  • 長さについてお詫び申し上げます。


古い質問

要するに:私は_clock_gettime_を使用して多くのコードセグメントの実行時間を測定しています。別々の実行間で非常に一貫性のない測定が発生しています。このメソッドは、他のメソッドと比較した場合、非常に高い標準偏差を持っています(以下の説明を参照)。

質問:_clock_gettime_が他の方法と比較して非常に一貫性のない測定値を与える理由はありますか?スレッドのアイドル時間を考慮した同じ解像度の代替方法はありますか?

説明:Cコードのいくつかの小さな部分をプロファイリングしようとしています。各コードセグメントの実行時間は、数マイクロ秒以下です。 1回の実行で、各コードセグメントは数百回実行され、_runs × hundreds_の測定値が生成されます。

また、スレッドが実際に実行に費やした時間のみを測定する必要があります(そのため、rdtscは適切ではありません)。高解像度も必要です(そのため、timesは適していません)。

私は次の方法を試しました:

  • rdtsc(LinuxおよびWindowsの場合)、

  • _clock_gettime_(with'CLOCK_THREAD_CPUTIME_ID '; Linuxの場合)、および

  • QueryThreadCycleTime(Windowsの場合)。

方法論:分析は25回の実行で実行されました。各実行で、個別のコードセグメントが101回繰り返されます。したがって、私は2525の測定値を持っています。次に、測定値のヒストグラムを確認し、いくつかの基本的なもの(平均、標準偏差、中央値、最頻値、最小値、最大値など)も計算します。

3つの方法の「類似性」をどのように測定したかについては説明しませんが、これには、各コードセグメントで費やされた時間の割合の基本的な比較が含まれます(「割合」は、時間が正規化されることを意味します)。次に、これらの比率の純粋な違いを調べます。この比較により、すべての「rdtsc」、「QTCT」、および「CGT」は、25回の実行で平均したときに同じ比率を測定することが示されました。ただし、以下の結果は、「CGT」の標準偏差が非常に大きいことを示しています。これにより、私のユースケースでは使用できなくなります。

結果

同じコードセグメントの_clock_gettime_とrdtscの比較(101回の測定を25回実行= 2525回の読み取り):

  • clock_gettime

    • 11 nsの1881測定、
    • 595回の測定は(ほぼ正常に分散)3369〜3414 nsでしたが、
    • 11680 nsの2回の測定、
    • 1506022 nsの1回の測定、および
    • 残りは900〜5000nsです。

    • 最小:11 ns

    • 最大:1506022 ns
    • 平均:1471.862 ns
    • 中央値:11 ns
    • モード:11 ns
    • Stddev:29991.034
  • rdtsc(注:この実行中にコンテキストスイッチは発生しませんでしたが、発生した場合、通常は30000ティック程度の1回の測定になります):

    • 274〜325ティックの1178測定、
    • 326から375ティックの間の306の測定、
    • 376から425ティックの間の910の測定、
    • 426から990ティックの間の129の測定、
    • 1240ティックの1回の測定、および
    • 1256ティックの1回の測定。

    • 最小:274ティック

    • 最大:1256ティック
    • 平均:355.806ティック
    • 中央値:333ティック
    • モード:376ティック
    • Stddev:83.896

ディスカッション

  • rdtscは、LinuxとWindowsの両方で非常によく似た結果をもたらします。許容できる標準偏差があります。実際には非常に一貫性があり、安定しています。ただし、スレッドのアイドル時間は考慮されていません。したがって、コンテキストスイッチによって測定が不安定になります(Windowsでは、これを頻繁に観察しています。平均1000ティック程度のコードセグメントでは、プリエンプションが原因で、時々最大30000ティックかかります)。

  • QueryThreadCycleTimeは、非常に一貫性のある測定値を提供します。 rdtscと比較すると、標準偏差がはるかに低くなっています。コンテキストスイッチが発生しない場合、このメソッドはrdtscとほぼ同じです。

  • 一方、_clock_gettime_は、非常に一貫性のない結果を生成しています(実行間だけでなく、測定間でも)。標準偏差は極端です(rdtscと比較した場合)。

統計が大丈夫だといいのですが。しかし、2つの方法の間の測定値のそのような不一致の理由は何でしょうか?もちろん、キャッシング、CPU /コアの移行などがあります。しかし、これは「rdtsc」と「clock_gettime」の間のそのような違いの原因ではありません。何が起こっている?


さらなる調査

私はこれをもう少し調査しました。私は2つのことをしました:

  1. clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t)を呼び出すだけのオーバーヘッドを測定し(付録のコード1を参照)、

  2. _clock_gettime_と呼ばれる単純なループで、読み取り値を配列に格納しました(付録のコード2を参照)。 デルタ時間を測定します(連続する測定時間の違い。これは、_clock_gettime_の呼び出しのオーバーヘッドに少し対応するはずです)。

2つの異なるLinuxカーネルバージョンを備えた2つの異なるコンピューターで測定しました。

[〜#〜] cgt [〜#〜]

  1. [〜#〜] cpu [〜#〜]:Core 2 Duo L9400 @ 1.86GHz

    カーネル:Linux 2.6.40-4.fc15.i686#1SMP金7月29日18:54:39UTC 2011 i686 i686 i386

    結果

    • 推定_clock_gettime_オーバーヘッド:690〜710 ns
    • デルタ時間

      • 平均:815.22 ns
      • 中央値:713 ns
      • モード:709 ns
      • 最小:698 ns
      • 最大:23359 ns
      • ヒストグラム(省略された範囲の頻度は0):

        _      Range       |  Frequency
        ------------------+-----------
          697 < x ≤ 800   ->     78111  <-- cached?
          800 < x ≤ 1000  ->     16412
         1000 < x ≤ 1500  ->         3
         1500 < x ≤ 2000  ->      4836  <-- uncached?
         2000 < x ≤ 3000  ->       305
         3000 < x ≤ 5000  ->       161
         5000 < x ≤ 10000 ->       105
        10000 < x ≤ 15000 ->        53
        15000 < x ≤ 20000 ->         8
        20000 < x         ->         5
        _
  2. [〜#〜] cpu [〜#〜]:4×デュアルコアAMDOpteronプロセッサ275

    カーネル:Linux 2.6.26-2-AMD64#1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux

    結果

    • 推定_clock_gettime_オーバーヘッド:279〜283 ns
    • デルタ時間

      • 平均:320.00
      • 中央値:1
      • モード:1
      • 最小:1
      • 最大:3495529
      • ヒストグラム(省略された範囲の頻度は0):

        _      Range         |  Frequency
        --------------------+-----------
                  x ≤ 1     ->     86738  <-- cached?
            282 < x ≤ 300   ->     13118  <-- uncached?
            300 < x ≤ 440   ->        78
           2000 < x ≤ 5000  ->        52
           5000 < x ≤ 30000 ->         5
        3000000 < x         ->         8
        _

[〜#〜] rdtsc [〜#〜]

関連コード_rdtsc_delta.c_および_rdtsc_overhead.c_。

  1. [〜#〜] cpu [〜#〜]:Core 2 Duo L9400 @ 1.86GHz

    カーネル:Linux 2.6.40-4.fc15.i686#1SMP金7月29日18:54:39UTC 2011 i686 i686 i386

    結果

    • 推定オーバーヘッド:39〜42ティック
    • デルタ時間

      • 平均:52.46ティック
      • 中央値:42ティック
      • モード:42ティック
      • 最小:35ティック
      • 最大:28700ティック
      • ヒストグラム(省略された範囲の頻度は0):

        _      Range       |  Frequency
        ------------------+-----------
           34 < x ≤ 35    ->     16240  <-- cached?
           41 < x ≤ 42    ->     63585  <-- uncached? (small difference)
           48 < x ≤ 49    ->     19779  <-- uncached?
           49 < x ≤ 120   ->       195
         3125 < x ≤ 5000  ->       144
         5000 < x ≤ 10000 ->        45
        10000 < x ≤ 20000 ->         9
        20000 < x         ->         2
        _
  2. [〜#〜] cpu [〜#〜]:4×デュアルコアAMDOpteronプロセッサ275

    カーネル:Linux 2.6.26-2-AMD64#1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux

    結果

    • 推定オーバーヘッド:13.7〜17.0ティック
    • デルタ時間

      • 平均:35.44ティック
      • 中央値:16ティック
      • モード:16ティック
      • 最小:14ティック
      • 最大:16372ティック
      • ヒストグラム(省略された範囲の頻度は0):

        _      Range       |  Frequency
        ------------------+-----------
           13 < x ≤ 14    ->       192
           14 < x ≤ 21    ->     78172  <-- cached?
           21 < x ≤ 50    ->     10818
           50 < x ≤ 103   ->     10624  <-- uncached?
         5825 < x ≤ 6500  ->        88
         6500 < x ≤ 8000  ->        88
         8000 < x ≤ 10000 ->        11
        10000 < x ≤ 15000 ->         4
        15000 < x ≤ 16372 ->         2
        _

[〜#〜] qtct [〜#〜]

関連コード_qtct_delta.c_および_qtct_overhead.c_。

  1. [〜#〜] cpu [〜#〜]:Core 2 6700 @ 2.66GHz

    カーネル:Windows 764ビット

    結果

    • 推定オーバーヘッド:890〜940ティック
    • デルタ時間

      • 平均:1057.30ティック
      • 中央値:890ティック
      • モード:890ティック
      • 最小:880ティック
      • 最大:29400ティック
      • ヒストグラム(省略された範囲の頻度は0):

        _      Range       |  Frequency
        ------------------+-----------
          879 < x ≤ 890   ->     71347  <-- cached?
          895 < x ≤ 1469  ->       844
         1469 < x ≤ 1600  ->     27613  <-- uncached?
         1600 < x ≤ 2000  ->        55
         2000 < x ≤ 4000  ->        86
         4000 < x ≤ 8000  ->        43
         8000 < x ≤ 16000 ->        10
        16000 < x         ->         1
        _

結論

私の質問に対する答えは、私のマシン(古いLinuxカーネルを搭載したAMD CPUを搭載したもの)のバグのある実装になると思います。

古いカーネルを搭載したAMDマシンのCGTの結果は、いくつかの極端な測定値を示しています。デルタ時間を見ると、最も頻繁なデルタは1nsであることがわかります。これは、_clock_gettime_の呼び出しに1ナノ秒もかからなかったことを意味します。さらに、それはまた、(3000000 nsを超える)非常に大きなデルタを多数生成しました!これは誤った動作のようです。 (多分、説明されていないコア移行?)

備考:

  • CGTとQTCTのオーバーヘッドはかなり大きいです。

  • CPUキャッシングは非常に大きな違いを生むように思われるため、オーバーヘッドを説明することも困難です。

  • たぶん、RDTSCに固執し、プロセスを1つのコアにロックし、リアルタイムの優先順位を割り当てることが、コードの一部が使用したサイクル数を知るための最も正確な方法です...


付録

コード1:_clock_gettime_overhead.c_

_#include <time.h>
#include <stdio.h>
#include <stdint.h>

/* Compiled & executed with:

    gcc clock_gettime_overhead.c -O0 -lrt -o clock_gettime_overhead
    ./clock_gettime_overhead 100000
*/

int main(int argc, char **args) {
    struct timespec tstart, tend, dummy;
    int n, N;
    N = atoi(args[1]);
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tstart);
    for (n = 0; n < N; ++n) {
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
    }
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tend);
    printf("Estimated overhead: %lld ns\n",
            ((int64_t) tend.tv_sec * 1000000000 + (int64_t) tend.tv_nsec
                    - ((int64_t) tstart.tv_sec * 1000000000
                            + (int64_t) tstart.tv_nsec)) / N / 10);
    return 0;
}
_

コード2:_clock_gettime_delta.c_

_#include <time.h>
#include <stdio.h>
#include <stdint.h>

/* Compiled & executed with:

    gcc clock_gettime_delta.c -O0 -lrt -o clock_gettime_delta
    ./clock_gettime_delta > results
*/

#define N 100000

int main(int argc, char **args) {
    struct timespec sample, results[N];
    int n;
    for (n = 0; n < N; ++n) {
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &sample);
        results[n] = sample;
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n",
               (int64_t) results[n].tv_sec * 1000000000 + 
                   (int64_t)results[n].tv_nsec,
               (int64_t) results[n].tv_sec * 1000000000 + 
                   (int64_t) results[n].tv_nsec - 
                   ((int64_t) results[n-1].tv_sec * 1000000000 + 
                        (int64_t)results[n-1].tv_nsec));
    }
    return 0;
}
_

コード:_rdtsc.h_

_static uint64_t rdtsc() {
#if defined(__GNUC__)
#   if defined(__i386__)
    uint64_t x;
    __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
    return x;
#   Elif defined(__x86_64__)
    uint32_t hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)lo) | ((uint64_t)hi << 32);
#   else
#       error Unsupported architecture.
#   endif
#Elif defined(_MSC_VER)
    return __rdtsc();
#else
#   error Other compilers not supported...
#endif
}
_

コード4:_rdtsc_delta.c_

_#include <stdio.h>
#include <stdint.h>
#include "rdtsc.h"

/* Compiled & executed with:

    gcc rdtsc_delta.c -O0 -o rdtsc_delta
    ./rdtsc_delta > rdtsc_delta_results

Windows:

    cl -Od rdtsc_delta.c
    rdtsc_delta.exe > windows_rdtsc_delta_results
*/

#define N 100000

int main(int argc, char **args) {
    uint64_t results[N];
    int n;
    for (n = 0; n < N; ++n) {
        results[n] = rdtsc();
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
    }
    return 0;
}
_

コード5:_rdtsc_overhead.c_

_#include <time.h>
#include <stdio.h>
#include <stdint.h>
#include "rdtsc.h"

/* Compiled & executed with:

    gcc rdtsc_overhead.c -O0 -lrt -o rdtsc_overhead
    ./rdtsc_overhead 1000000 > rdtsc_overhead_results

Windows:

    cl -Od rdtsc_overhead.c
    rdtsc_overhead.exe 1000000 > windows_rdtsc_overhead_results
*/

int main(int argc, char **args) {
    uint64_t tstart, tend, dummy;
    int n, N;
    N = atoi(args[1]);
    tstart = rdtsc();
    for (n = 0; n < N; ++n) {
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
    }
    tend = rdtsc();
    printf("%G\n", (double)(tend - tstart)/N/10);
    return 0;
}
_

コード6:_qtct_delta.c_

_#include <stdio.h>
#include <stdint.h>
#include <Windows.h>

/* Compiled & executed with:

    cl -Od qtct_delta.c
    qtct_delta.exe > windows_qtct_delta_results
*/

#define N 100000

int main(int argc, char **args) {
    uint64_t ticks, results[N];
    int n;
    for (n = 0; n < N; ++n) {
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        results[n] = ticks;
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
    }
    return 0;
}
_

コード7:_qtct_overhead.c_

_#include <stdio.h>
#include <stdint.h>
#include <Windows.h>

/* Compiled & executed with:

    cl -Od qtct_overhead.c
    qtct_overhead.exe 1000000
*/

int main(int argc, char **args) {
    uint64_t tstart, tend, ticks;
    int n, N;
    N = atoi(args[1]);
    QueryThreadCycleTime(GetCurrentThread(), &tstart);
    for (n = 0; n < N; ++n) {
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
    }
    QueryThreadCycleTime(GetCurrentThread(), &tend);
    printf("%G\n", (double)(tend - tstart)/N/10);
    return 0;
}
_
35
sinharaj

CLOCK_THREAD_CPUTIME_IDrdtscを使用して実装されているのと同様に、同じ問題が発生する可能性があります。 clock_gettimeのマニュアルページには次のように書かれています。

CLOCK_PROCESS_CPUTIME_IDおよびCLOCK_THREAD_CPUTIME_IDクロックは、CPU(i386のTSC、ItaniumのAR.ITC)からのタイマーを使用して、多くのプラットフォームで実現されます。これらのレジスタはCPU間で異なる可能性があり、その結果、プロセスが別のCPUに移行された場合、これらのクロックは偽の結果を返す可能性があります。

それがあなたの問題を説明するかもしれないように聞こえますか?安定した結果を得るには、プロセスを1つのCPUにロックする必要がありますか?

5
TomH

負になり得ない非常に偏った分布がある場合、平均、中央値、最頻値の間に大きな不一致が見られます。このような分布では、標準偏差はかなり無意味です。

通常、対数変換することをお勧めします。それはそれを「より正常」にするでしょう。

0
Mike Dunlavey