ほとんどのデスクトップおよびサーバーアーキテクチャでは、分岐予測や投機的実行を行うため、分岐は間接呼び出しよりも高速です。間接呼び出しが単一のブランチよりも高速なアーキテクチャについては聞いたことがありません。(ステートメントのジャンプテーブルにswitch()
は複数のブランチがあるため、まったく別のものです。)
私が一緒に投げた次のマイクロベンチマークを考えてみましょう。test.c
:
/* test.c */
volatile long test_calls = 0L;
volatile long test_sum = 0L;
void test(long counter)
{
test_calls++;
test_sum += counter;
}
work.c
:
/* work.c */
void test(long counter);
/* Work function, to be measured */
void test_work(long counter, int flag)
{
if (flag)
test(counter);
}
/* Dummy function, to measure call overhead */
void test_none(long counter __attribute__((unused)), int flag __attribute__((unused)) )
{
return;
}
およびharness.c
:
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
/* From test.c */
extern volatile long test_calls;
extern volatile long test_sum;
/* Dummy function, to measure call overhead */
void test_none(long counter, int flag);
/* Work function, to be measured */
void test_work(long counter, int flag);
/* Timing harness -- GCC x86; modify for other architectures */
struct timing {
struct timespec wall_start;
struct timespec wall_stop;
uint64_t cpu_start;
uint64_t cpu_stop;
};
static inline void start_timing(struct timing *const mark)
{
clock_gettime(CLOCK_REALTIME, &(mark->wall_start));
mark->cpu_start = __builtin_ia32_rdtsc();
}
static inline void stop_timing(struct timing *const mark)
{
mark->cpu_stop = __builtin_ia32_rdtsc();
clock_gettime(CLOCK_REALTIME, &(mark->wall_stop));
}
static inline double cpu_timing(const struct timing *const mark)
{
return (double)(mark->cpu_stop - mark->cpu_start); /* Cycles */
}
static inline double wall_timing(const struct timing *const mark)
{
return (double)(mark->wall_stop.tv_sec - mark->wall_start.tv_sec)
+ (double)(mark->wall_stop.tv_nsec - mark->wall_start.tv_nsec) / 1000000000.0;
}
static int cmpdouble(const void *aptr, const void *bptr)
{
const double a = *(const double *)aptr;
const double b = *(const double *)bptr;
if (a < b)
return -1;
else
if (a > b)
return +1;
else
return 0;
}
void report(double *const wall, double *const cpu, const size_t count)
{
printf("\tInitial call: %.0f cpu cycles, %.9f seconds real time\n", cpu[0], wall[0]);
qsort(wall, count, sizeof (double), cmpdouble);
qsort(cpu, count, sizeof (double), cmpdouble);
printf("\tMinimum: %.0f cpu cycles, %.9f seconds real time\n", cpu[0], wall[0]);
printf("\t5%% less than %.0f cpu cycles, %.9f seconds real time\n", cpu[count/20], wall[count/20]);
printf("\t25%% less than %.0f cpu cycles, %.9f seconds real time\n", cpu[count/4], wall[count/4]);
printf("\tMedian: %.0f cpu cycles, %.9f seconds real time\n", cpu[count/2], wall[count/2]);
printf("\t75%% less than %.0f cpu cycles, %.9f seconds real time\n", cpu[count-count/4-1], wall[count-count/4-1]);
printf("\t95%% less than %.0f cpu cycles, %.9f seconds real time\n", cpu[count-count/20-1], wall[count-count/20-1]);
printf("\tMaximum: %.0f cpu cycles, %.9f seconds real time\n", cpu[count-1], wall[count-1]);
}
int main(int argc, char *argv[])
{
struct timing measurement;
double *wall_seconds = NULL;
double *cpu_cycles = NULL;
unsigned long count = 0UL;
unsigned long i;
int flag;
char dummy;
if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s COUNT FLAG\n", argv[0]);
fprintf(stderr, "\n");
return 1;
}
if (sscanf(argv[1], " %lu %c", &count, &dummy) != 1) {
fprintf(stderr, "%s: Invalid COUNT.\n", argv[1]);
return 1;
}
if (count < 1UL) {
fprintf(stderr, "%s: COUNT is too small.\n", argv[1]);
return 1;
}
if (!(unsigned long)(count + 1UL)) {
fprintf(stderr, "%s: COUNT is too large.\n", argv[1]);
return 1;
}
if (sscanf(argv[2], " %d %c", &flag, &dummy) != 1) {
fprintf(stderr, "%s: Invalid FLAG.\n", argv[2]);
return 1;
}
wall_seconds = malloc(sizeof (double) * (size_t)count);
cpu_cycles = malloc(sizeof (double) * (size_t)count);
if (!wall_seconds || !cpu_cycles) {
free(cpu_cycles);
free(wall_seconds);
fprintf(stderr, "Cannot allocate enough memory. Try smaller COUNT.\n");
return 1;
}
printf("Call and measurement overhead:\n");
fflush(stdout);
for (i = 0UL; i < count; i++) {
start_timing(&measurement);
test_none(i, flag);
stop_timing(&measurement);
wall_seconds[i] = wall_timing(&measurement);
cpu_cycles[i] = cpu_timing(&measurement);
}
report(wall_seconds, cpu_cycles, (size_t)count);
printf("\n");
printf("Measuring FLAG==0 calls: ");
fflush(stdout);
test_calls = 0L;
test_sum = 0L;
for (i = 0UL; i < count; i++) {
start_timing(&measurement);
test_work(i, 0);
stop_timing(&measurement);
wall_seconds[i] = wall_timing(&measurement);
cpu_cycles[i] = cpu_timing(&measurement);
}
printf("%ld calls, sum %ld.\n", test_calls, test_sum);
report(wall_seconds, cpu_cycles, (size_t)count);
printf("\n");
printf("Measuring FLAG==%d calls:", flag);
fflush(stdout);
test_calls = 0L;
test_sum = 0L;
for (i = 0UL; i < count; i++) {
start_timing(&measurement);
test_work(i, flag);
stop_timing(&measurement);
wall_seconds[i] = wall_timing(&measurement);
cpu_cycles[i] = cpu_timing(&measurement);
}
printf("%ld calls, sum %ld.\n", test_calls, test_sum);
report(wall_seconds, cpu_cycles, (size_t)count);
printf("\n");
printf("Measuring alternating FLAG calls: ");
fflush(stdout);
test_calls = 0L;
test_sum = 0L;
for (i = 0UL; i < count; i++) {
start_timing(&measurement);
test_work(i, i & 1);
stop_timing(&measurement);
wall_seconds[i] = wall_timing(&measurement);
cpu_cycles[i] = cpu_timing(&measurement);
}
printf("%ld calls, sum %ld.\n", test_calls, test_sum);
report(wall_seconds, cpu_cycles, (size_t)count);
printf("\n");
free(cpu_cycles);
free(wall_seconds);
return 0;
}
3つのファイルを空のディレクトリに置き、コンパイルしてビルドし./call-test
ます。
rm -f *.o
gcc -W -Wall -O3 -fomit-frame-pointer -c harness.c
gcc -W -Wall -O3 -fomit-frame-pointer -c work.c
gcc -W -Wall -O3 -fomit-frame-pointer -c test.c
gcc harness.o work.o test.o -lrt -o call-test
AMD Athlon II X4 640で、gcc-4.6.3(Xubuntu 10.04)を使用して、実行中
./call-test 1000000 1
test_calls
オーバーヘッドは、テストのみ(ブランチは使用されない)ではわずか2クロックサイクル(<1ns)であり、カウンターを増やして追加する2番目の関数を呼び出すとわずか4クロックサイクル(ナノ秒強)であることがわかりtest_sum
ます。
すべての最適化を省略すると(コンパイル時に使用-O0
および除外-fomit-frame-pointer
)、テストだけで約3クロックサイクル(分岐が行われない場合は3サイクル)、分岐が行われて2つの追加変数を更新する作業が行われる場合は約9サイクルかかります。 。
(2つの追加の変数を使用すると、ハーネスが実際に実行する必要があるすべてのことを実行していることが簡単にわかります。これらは単なる追加のチェックです。2番目の関数で作業を行いたいので、タイミングの違いを見つけやすくなります。 )。
上記の解釈は、コードがすでにキャッシュされている場合にのみ有効です。つまり、最近実行します。コードがめったに実行されない場合、コードはキャッシュにありません。ただし、テストのオーバーヘッドはさらに重要ではありません。キャッシング効果-たとえば、「近くの」コードが実行された場合(これは呼び出しオーバーヘッド測定で確認できますが、他のテスト関数コードもキャッシュされる傾向があります!)-とにかくはるかに大きくなります。(テストハーネスは最初の呼び出し結果を個別に生成しますが、キャッシュをクリアしようとしないため、あまり信頼しないでください。)
私の結論は、
if (flag)
debug_function_call();
通常のコードではまったく問題ありません。オーバーヘッドは文字通り無視できます。実質的に無関係です。いつものように、代わりに全体的なアルゴリズムを検討してください。アルゴリズムの機能強化は、コンパイラーが生成するコードについて心配するよりもはるかに大きな見返りをもたらします。
(上記のテストコードを一度に作成したので、バグやブレインファートが含まれている可能性があります。コードを修正できるように、確認してください。見つかった場合は、以下にお知らせください。)