C 言語のラッパー関数オーバーヘッド
プログラムを組む際、ラッパー関数を作ることは良くある。
このラッパー関数のオーバーヘッドが気になったので簡単に調べてみた。
計測用サンプルは次の通り。
#include<stdio.h>
typedef void (func_t)( int val1, int val2 );
void func( int val1, int val2 )
{
printf( "%d %d", val1, val2 );
}
void wrapper0( int val1, int val2 )
{
func( val1, val2 );
}
void wrapper1( func_t * pFunc, int val1, int val2 )
{
pFunc( val1, val2 );
}
void wrapper2( int val1, int val2, func_t * pFunc )
{
pFunc( val1, val2 );
}
main() {
wrapper0( 0, 1 );
wrapper1( func, 0, 1 );
wrapper2( 0, 1, func );
}
関数 func() をコールする 3 種類のラッパー関数 wrapper0, wrapper1, wrapper2 を用意した。
それぞれのラッパー関数は次の形になっている。
ラッパー | 引数 |
---|---|
wrapper0 | 呼び出し先と同じ引数 |
wrapper1 | ラッパー独自引数の後に呼び出し先と同じ引数 |
wrapper2 | 呼び出し先と同じ引数の後にラッパー独自引数 |
これを gcc の x64 で -O の最適化した結果が次になる。 (func の処理は省略)
0000000000000021 <wrapper0>:
21: 48 83 ec 08 sub $0x8,%rsp
25: e8 00 00 00 00 callq 2a <wrapper0+0x9>
2a: 48 83 c4 08 add $0x8,%rsp
2e: c3 retq
000000000000002f <wrapper1>:
2f: 48 83 ec 08 sub $0x8,%rsp
33: 48 89 f8 mov %rdi,%rax
36: 89 f7 mov %esi,%edi
38: 89 d6 mov %edx,%esi
3a: ff d0 callq *%rax
3c: 48 83 c4 08 add $0x8,%rsp
40: c3 retq
0000000000000041 <wrapper2>:
41: 48 83 ec 08 sub $0x8,%rsp
45: ff d2 callq *%rdx
47: 48 83 c4 08 add $0x8,%rsp
4b: c3 retq
000000000000004c <main>:
4c: 48 83 ec 08 sub $0x8,%rsp
50: be 01 00 00 00 mov $0x1,%esi
55: bf 00 00 00 00 mov $0x0,%edi
5a: e8 00 00 00 00 callq 5f <main+0x13>
5f: ba 01 00 00 00 mov $0x1,%edx
64: be 00 00 00 00 mov $0x0,%esi
69: bf 00 00 00 00 mov $0x0,%edi
6e: e8 00 00 00 00 callq 73 <main+0x27>
73: ba 00 00 00 00 mov $0x0,%edx
78: be 01 00 00 00 mov $0x1,%esi
7d: bf 00 00 00 00 mov $0x0,%edi
82: e8 00 00 00 00 callq 87 <main+0x3b>
87: b8 00 00 00 00 mov $0x0,%eax
8c: 48 83 c4 08 add $0x8,%rsp
90: c3 retq
上記通り wrapper0 と wrapper2 は、ほぼ同じコードになっており、 wrapper1 は引数をずらす処理が余分に入っている。
想像通りの結果といえば想像通りだが、 ちゃんと最適化された処理になっている。
以上のことから言えることは、 ラッパー関数独自の引数は、先頭ではなく末尾にもっていった方が良いということだ。
ただし、ここまで最適化が効くケースは、 ラッパー関数内での目的の関数コールが先頭にある場合に限られるので、 目的の関数コールを先頭に持ってこれない場合は、気にしないで良いだろう。
なお、 -O2 で最適化をかけると wrapper1, wrapper2 は次の処理に最適化された。
0000000000000030 <wrapper1>:
30: 48 89 f8 mov %rdi,%rax
33: 89 f7 mov %esi,%edi
35: 89 d6 mov %edx,%esi
37: ff e0 jmpq *%rax
39: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000000040 <wrapper2>:
40: ff e2 jmpq *%rdx
個人的には、こっちの方が納得がいく。
また、次のようにラッパー関数に static 宣言を付加して、 外部からコールされないことを明示すると、
#include<stdio.h>
typedef void (func_t)( int val1, int val2 );
void func( int val1, int val2 )
{
printf( "%d %d", val1, val2 );
}
static void wrapper0( int val1, int val2 )
{
func( val1, val2 );
}
static void wrapper1( func_t * pFunc, int val1, int val2 )
{
pFunc( val1, val2 );
}
static void wrapper2( int val1, int val2, func_t * pFunc )
{
pFunc( val1, val2 );
}
main() {
wrapper0( 0, 1 );
wrapper1( func, 0, 1 );
wrapper2( 0, 1, func );
}
出力結果は次のように、 ラッパーがインライン展開され、 ラッパーの引数の違いによる差分は無くなった。
0000000000000021 <main>:
21: 48 83 ec 08 sub $0x8,%rsp
25: be 01 00 00 00 mov $0x1,%esi
2a: bf 00 00 00 00 mov $0x0,%edi
2f: e8 00 00 00 00 callq 34 <main+0x13>
34: be 01 00 00 00 mov $0x1,%esi
39: bf 00 00 00 00 mov $0x0,%edi
3e: e8 00 00 00 00 callq 43 <main+0x22>
43: be 01 00 00 00 mov $0x1,%esi
48: bf 00 00 00 00 mov $0x0,%edi
4d: e8 00 00 00 00 callq 52 <main+0x31>
52: b8 00 00 00 00 mov $0x0,%eax
57: 48 83 c4 08 add $0x8,%rsp
5b: c3 retq
基本的に、ソースコードはメンテナンス性や可読性を優先すべきだが、 ソースコードを自動生成するような場合は、 このような細かいことも意識しておいた方が良いだろう。
以上。