ある関数が別の関数を呼び出して終了したとき、コンパイラは、「末尾呼び出しの最適化」を行うことができます。その結果、呼び出し元のスタックフレームを呼び出された関数で再利用できるようになります。この手続きは、SPARC アーキテクチャでよく行われます。SPARC アーキテクチャのコンパイラは、レジスタウィンドウの負荷を極力抑えるため、呼び出される側の関数で呼び出し元のレジスタウィンドウを再利用します。
この最適化により、呼び出し元関数の return プローブは、呼び出される側の entry プローブより先に起動するようになります。この順序は、かなり混乱を招きやすくなっています。たとえば、特定の関数から呼び出されたすべての関数と、この特定の関数が呼び出すすべての関数を記録する場合、次のようなスクリプトを使用します。
fbt::foo:entry { self->traceme = 1; } fbt:::entry /self->traceme/ { printf("called %s", probefunc); } fbt::foo:return /self->traceme/ { self->traceme = 0; }
しかし、foo() が最適化された末尾呼び出しで終わる場合、末尾で呼び出された関数と、この関数によって呼び出された関数は捕捉されません。動的にカーネルの最適化を解除することはできません。また、DTrace は、コードの構造を偽ることを望みません。このため、末尾呼び出しの最適化がいつ行われるかを意識する必要があります。
末尾呼び出しの最適化は、通常、次の例のようなソースコードで行われます。
return (bar());
次のようなソースコードで行われる場合もあります。
(void) bar(); return;
逆に、次の例のような終わり方の関数ソースコードでは、bar() の呼び出しを最適化することができません。これは、bar() の呼び出しが末尾呼び出しではないためです。
bar(); return (rval);
末尾呼び出しの最適化が行われているかどうかは、次のようにして判断できます。
DTrace の実行中に、問題の return プローブの arg0 をトレースします。arg0 には、関数内の復帰命令のオフセットが格納されます。
DTrace の停止後、mdb(1) を使って関数を調べます。トレースされたオフセットに、この関数からの復帰命令ではなく、別の関数の呼び出しが含まれている場合、末尾呼び出しの最適化が行われています。
命令セットのアーキテクチャ上の理由から、末尾呼び出しの最適化は、x86 システムよりも SPARC システムでよく使用されます。以下は、mdb を使って、カーネルの dup() 関数内で末尾呼び出しの最適化を検出する例です。
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}' |
このコマンドの実行中に、bash プロセスなど、dup(2) を実行するプログラムを実行します。このコマンドからは、次のような出力が得られます。
dup+0x10 ^C |
mdb を使って関数を調べましょう。
# echo "dup::dis" | mdb -k dup: sra %o0, 0, %o0 dup+4: mov %o7, %g1 dup+8: clr %o2 dup+0xc: clr %o1 dup+0x10: call -0x1278 <fcntl> dup+0x14: mov %g1, %o7 |
dup+0x10 が fcntl() 関数の呼び出しであり、ret 命令でないことが、この出力からわかります。したがって、fcntl() の呼び出しは、末尾呼び出しの最適化の一例になっています。