セクション古い質問には最初の質問(さらなる調査と結論が含まれています以来追加)。
さまざまなタイミング方法(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:
残りは900〜5000nsです。
最小:11 ns
rdtsc(注:この実行中にコンテキストスイッチは発生しませんでしたが、発生した場合、通常は30000ティック程度の1回の測定になります):
1256ティックの1回の測定。
最小:274ティック
ディスカッション:
rdtsc
は、LinuxとWindowsの両方で非常によく似た結果をもたらします。許容できる標準偏差があります。実際には非常に一貫性があり、安定しています。ただし、スレッドのアイドル時間は考慮されていません。したがって、コンテキストスイッチによって測定が不安定になります(Windowsでは、これを頻繁に観察しています。平均1000ティック程度のコードセグメントでは、プリエンプションが原因で、時々最大30000ティックかかります)。
QueryThreadCycleTime
は、非常に一貫性のある測定値を提供します。 rdtsc
と比較すると、標準偏差がはるかに低くなっています。コンテキストスイッチが発生しない場合、このメソッドはrdtsc
とほぼ同じです。
一方、_clock_gettime
_は、非常に一貫性のない結果を生成しています(実行間だけでなく、測定間でも)。標準偏差は極端です(rdtsc
と比較した場合)。
統計が大丈夫だといいのですが。しかし、2つの方法の間の測定値のそのような不一致の理由は何でしょうか?もちろん、キャッシング、CPU /コアの移行などがあります。しかし、これは「rdtsc」と「clock_gettime」の間のそのような違いの原因ではありません。何が起こっている?
私はこれをもう少し調査しました。私は2つのことをしました:
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t)
を呼び出すだけのオーバーヘッドを測定し(付録のコード1を参照)、
_clock_gettime
_と呼ばれる単純なループで、読み取り値を配列に格納しました(付録のコード2を参照)。 デルタ時間を測定します(連続する測定時間の違い。これは、_clock_gettime
_の呼び出しのオーバーヘッドに少し対応するはずです)。
2つの異なるLinuxカーネルバージョンを備えた2つの異なるコンピューターで測定しました。
[〜#〜] cgt [〜#〜]:
[〜#〜] 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デルタ時間:
ヒストグラム(省略された範囲の頻度は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
_
[〜#〜] 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デルタ時間:
ヒストグラム(省略された範囲の頻度は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
_。
[〜#〜] cpu [〜#〜]:Core 2 Duo L9400 @ 1.86GHz
カーネル:Linux 2.6.40-4.fc15.i686#1SMP金7月29日18:54:39UTC 2011 i686 i686 i386
結果:
デルタ時間:
ヒストグラム(省略された範囲の頻度は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
_
[〜#〜] cpu [〜#〜]:4×デュアルコアAMDOpteronプロセッサ275
カーネル:Linux 2.6.26-2-AMD64#1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux
結果:
デルタ時間:
ヒストグラム(省略された範囲の頻度は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
_。
[〜#〜] cpu [〜#〜]:Core 2 6700 @ 2.66GHz
カーネル:Windows 764ビット
結果:
デルタ時間:
ヒストグラム(省略された範囲の頻度は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;
}
_
CLOCK_THREAD_CPUTIME_ID
はrdtsc
を使用して実装されているのと同様に、同じ問題が発生する可能性があります。 clock_gettime
のマニュアルページには次のように書かれています。
CLOCK_PROCESS_CPUTIME_IDおよびCLOCK_THREAD_CPUTIME_IDクロックは、CPU(i386のTSC、ItaniumのAR.ITC)からのタイマーを使用して、多くのプラットフォームで実現されます。これらのレジスタはCPU間で異なる可能性があり、その結果、プロセスが別のCPUに移行された場合、これらのクロックは偽の結果を返す可能性があります。
それがあなたの問題を説明するかもしれないように聞こえますか?安定した結果を得るには、プロセスを1つのCPUにロックする必要がありますか?
負になり得ない非常に偏った分布がある場合、平均、中央値、最頻値の間に大きな不一致が見られます。このような分布では、標準偏差はかなり無意味です。
通常、対数変換することをお勧めします。それはそれを「より正常」にするでしょう。