関数ポインタのオーバーヘッド

Page content

現在 LuneScript の C 言語へのトランスコンパイル処理を対応中だが、 トランスコンパイルする際に関数ポインタによる関数コールのオーバーヘッドが どの程度なのか気になったので調べてみた。

結果

初めに結果から書くと、

関数ポインタによる関数コールのオーバーヘッドは、
通常の関数コールに比べて約 1.267 倍となることが判った。

この数値は、あくまで今回の実験結果であって、 関数ポインタかどうかの違いだけはなく、他の要因も入ってしまっている。 また、実行環境によっても差は出てくるだろう。

しかし、それでも目安程度にはなるだろう。

所感

論理的に考えて、関数ポインタの関数コールが通常の関数コールに比べて 遅くなることは理解していたが、これまで調べたことはなかった。 それが、今回の実験で明かになった。

個人的にはもっと差が出るかと思ったが、案外少ない結果になった。 これは、実験用コードが小さ過ぎて全てキャッシュに乗ってしまっているのが一番の要因だとは思う。 とはいえ、明らかなオーバーヘッドがあることには違いない。

プログラミングをしていれば感じていることだと思うが、 プログラムは関数コールの塊だ。

つまり、関数コールのオーバーヘッドは、 そのままプログラム全体の性能低下に直結する。

「関数ポインタ」というと、あまり使わっていないイメージを持つ人も多いかもしれないが、 オブジェクト指向の「ポリモーフィズム」あるいは「多態性」というと、 良く使っているイメージがあるのではないだろうか?

関数ポインタなど動的に動作が変わる処理は、 目的の制御を実現する上で非常に重要だが、 コードの把握が難しくなったり、オーバーヘッドによる性能低下を引き起こす可能性がある。

関数ポインタと通常の関数は、その特性にあわせてどちらを使用するかの検討が必要だ。

今回の実験結果をうけて、それがより明らかになったと思う。

実験詳細

ここでは、今回の実験方法について説明する。

コード

実験用に次の C 言語コードを作成した。

1
2
3
4
5
6
7
8
void sub( void ) {
}
void func_direct( func_t * pFunc ) {
    sub();
}
void func_indirect( func_t * pFunc ) {
    pFunc();
}

func_direct() は sub() 関数を直接コールする関数で、 func_indirect() は sub() 関数を関数ポインタでコールする関数だ。

この両者の関数を実行したときの実行時間を比較している。

ちなみにコードの全体は次の通りである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <sys/time.h>
#include <time.h>
#include <stdio.h>

typedef void (func_t)( void );

double getTime( void ) {
    struct timeval tm;
    gettimeofday( &tm, NULL );
    return tm.tv_sec + tm.tv_usec / 1000000.0;
}
void sub( void ) {
}
void func_direct( func_t * pFunc ) {
    sub();
}
void func_indirect( func_t * pFunc ) {
    pFunc();
}
void func_none( func_t * pFunc ) {
}
int main( int argc, const char * argv[] ) {
    long long loop;
    const char * pMode;

    double prev = getTime();
    switch ( argc ) {
    case 1:
        pMode = "indirect";
        for ( loop = 0; loop < 1000 * 1000 * 1000 * 2; loop++ ) {
            func_indirect( sub );
        }
        break;
    case 2:
        pMode = "direct";
        for ( loop = 0; loop < 1000 * 1000 * 1000 * 2; loop++ ) {
            func_direct( sub );
        }
        break;
    case 3:
        pMode = "none";
        for ( loop = 0; loop < 1000 * 1000 * 1000 * 2; loop++ ) {
            func_none( sub );
        }
        break;
    }
    printf( "%s: time = %g\n", pMode, getTime() - prev );
    return 0;
}

このプログラムは、コマンドラインの引数によって func_direct(), func_indirect(), func_none() のいずれかを 所定の回数分実行し、実行時間を表示する。

ちなみに func_none() は、関数ポインタと通常の関数コールの差を出す際に、 できるだけ他の要因を除外するために作成した関数だ。

計測結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
indirect: time = 11.4617
indirect: time = 11.2905
indirect: time = 11.2595
indirect: time = 11.3391
indirect: time = 11.3123
direct: time = 10.5253
direct: time = 10.5927
direct: time = 10.5389
direct: time = 10.6043
direct: time = 10.5259
none: time = 7.64467
none: time = 7.60627
none: time = 7.75474
none: time = 7.60123
none: time = 7.63887

これは、コマンドライン引数を変えて上記のプログラムをそれぞれ 5 回ずつ実行した結果だ。

それぞれを平均すると次のようになる。

時間(秒) 関数コールの時間(秒)
関数ポインタ 11.333 3.683
通常関数コール 10.557 2.908
関数コールなし 7.649

上記の「関数コールの時間」は、計測した時間から「関数コールなし」の時間を引いたものだ。

つまり、 for 分の制御などの関数ポインタのオーバーヘッドとは直接関係ない処理の時間を引いている。

この結果をもとに、次の計算をすると

(/ 3.683 2.908) 1.266506189821183

関数ポインタによる関数コールのオーバーヘッドは、 通常の関数コールに比べて 約 1.267 倍 となる。

以上