1

実行時に C コードから関数呼び出しを完全に削除し、必要に応じて挿入し直すことは可能ですか?

関数を使用しない場合に CPU サイクルが無駄にならないように、実行時に ELF を変更できるかどうかはわかりません。

関数の呼び出しを避けるために、関数呼び出しの前に「if」チェックを配置したくありません。

たとえば、グローバル フラグ g_flg=1 の場合、func1 は次のようになります。

void func1(int x)
{
 /* some processing */

 func2(y);

 /* some processing */

}

グローバル g_flag=0 の場合、func1 は次のようになります。

void func1(int x)
{
 /* some processing */

  /* some processing */

}
4

3 に答える 3

2

それを必要としないものを最適化しないでください。パフォーマンスの潜在的な改善を評価してみましたか?

g_flgを1に設定して、これを実行してみてください。

if (g_flg == 1) {func2(y);}

次に、これを実行してみてください。

func2(y);

両方とも100万回(または妥当な時間内に実行できる回数)。両者の間に実質的な違いはないことに気付くと思います。

さらに、それを除けば、ELFはバイナリ(コンパイル済み)形式であるため、やりたいことは不可能だと思います。

于 2012-10-02T13:50:05.497 に答える
1

代わりにやることでおそらく逃げることができるのは、次のようなものです。

struct Something;
typedef struct Something Something;

int myFunction(Something * me, int i)
{
    // do a bunch of stuff
    return 42; // obviously the answer
}

int myFunctionDoNothing(Something * dummy1, int dummy2)
{
    return 0;
}

int (*function)(Something *, int) = myFunctionDoNothing;

// snip to actual use of function

int i;

function = myFunctionDoNothing;
for (i = 0; i < 100000; ++i) function(NULL, 5 * i); // does nothing

function = myFunction;
for (i = 0; i < 100000; ++i) function(NULL, 5 * i); // does something

警告

これは時期尚早の最適化である可能性があります。コンパイラがこれをどのように処理するか、およびCPUが分岐を処理する方法によっては、単純な方法(フラグを使用して関数で停止する)とは対照的に、実際にはこの方法でパフォーマンスが低下する可能性があります。

于 2012-10-02T13:49:51.843 に答える
0

ほとんどのデスクトップおよびサーバーアーキテクチャでは、分岐予測や投機的実行を行うため、分岐は間接呼び出しよりも高速です。間接呼び出しが単一のブランチよりも高速なアーキテクチャについては聞いたことがありません。(ステートメントのジャンプテーブルに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();

通常のコードではまったく問題ありません。オーバーヘッドは文字通り無視できます。実質的に無関係です。いつものように、代わりに全体的なアルゴリズムを検討してください。アルゴリズムの機能強化は、コンパイラーが生成するコードについて心配するよりもはるかに大きな見返りをもたらします。

(上記のテストコードを一度に作成したので、バグやブレインファートが含まれている可能性があります。コードを修正できるように、確認してください。見つかった場合は、以下にお知らせください。)

于 2012-10-02T18:44:17.493 に答える