rdtsc で関数呼び出しの時間を測定したい。そこで、以下の2つの方法で測定しました。
- ループで呼び出します。ループ内の各 rdtsc 差を集計し、呼び出し数で割ります。(これをNとしましょう)
- ループで呼び出します。ループ自体の rdtsc 差を取得し、N で割ります。
しかし、一貫性のない動作がいくつか見られます。
- N を増やすと、方法 1 と 2 の両方で時間がかなり単調に減少します。方法 2 の場合、ループ制御のオーバーヘッドが償却されるという点で理解できます。しかし、方法1の場合はどうなるかわかりません。
- 実際、方法 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 は、方法 2 よりも収束が遅くなります。しかし、異なる N 設定の値になぜこのような劇的な違いがあるのか について、私は困惑しています。おそらく、現時点では見られないいくつかの基本的な間違いをここで行っています。
- 方法 1 の値は、実際には方法 2 よりもいくらか大きくなっています。ループ制御のオーバーヘッドが含まれていないため、方法 2 の値と同等かそれよりわずかに小さいと予想しました。
質問
要約すると、私の質問は
Nを増やすと、両方の方法で得られる値が大幅に変化するのはなぜですか? 特に、ループ制御のオーバーヘッドを考慮しない方法 1 の場合。
最初の方法が計算でループ制御のオーバーヘッドを除外しているのに、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. コメントの解決策。