当一个函数以调用另一个函数而结束时,编译器可能会进行尾部调用优化,优化后,被调用的函数将重用调用方的栈帧。此过程通常用于 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) 查看该函数。如果跟踪的偏移量包含对另一个函数的调用,而不是从函数返回的指令,则说明已对调用进行了尾部调用优化。
由于指令集体系结构,尾部调用优化在 SPARC 系统上要比在 x86 系统上更常见。以下示例使用 mdb 发现内核的 dup() 函数中的尾部调用优化:
# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}' |
运行此命令时,将运行执行 dup(2) 的程序(如 bash 进程)。上面的命令会提供与以下示例类似的输出:
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() 就是尾部调用优化的一个示例。