我经常听到人们说C语言不支持尾调用消除。虽然标准没有保证,但在任何体面的实现中,它都能够被执行吗?假设你只关注成熟、良好实现的编译器,并且不关心对于为较为晦涩的平台编写的原始编译器的最大可移植性,那么在C语言中依赖尾调用消除是否合理?
此外,为什么标准将尾调用优化排除在外呢?
我经常听到人们说C语言不支持尾调用消除。虽然标准没有保证,但在任何体面的实现中,它都能够被执行吗?假设你只关注成熟、良好实现的编译器,并且不关心对于为较为晦涩的平台编写的原始编译器的最大可移植性,那么在C语言中依赖尾调用消除是否合理?
此外,为什么标准将尾调用优化排除在外呢?
像 "C 不执行尾调用消除" 这样的说法是没有意义的。正如您自己正确地指出的那样,这取决于具体的实现方式。是的,任何合理的实现都可以将尾递归轻松转换成等效的循环。当然,C 编译器通常不会对每个代码片段进行什么优化和不优化的保证。您需要编译并自行查看。
%ebx
;其他平台也遵循这个想法。与使用普通调用的函数不同,使用尾调用的函数必须在跳转到子例程之前恢复被调用方保存的寄存器,而不是在返回自己时恢复。通常,这没有问题,因为在这个点上,最顶层的调用函数不关心%ebx
中存储的值,但是位置无关代码依赖于每个跳转、调用或分支命令中这个值。
其他问题可能涉及面向对象语言(如C++)中未完成的清理工作,这意味着函数中的最后一次调用实际上并不是最后一次调用,而是清理工作。因此,编译器通常在这种情况下不会进行优化。
当然,setjmp
和longjmp
也是有问题的,因为这实际上意味着一个函数可以执行多次,而不是只执行一次。这在编译时很难或不可能进行优化!
可能还有更多技术原因可以考虑。这些只是一些注意事项。
编译器通常可以识别函数在调用另一个函数之后不需要执行任何操作的情况,并将该调用替换为跳转。许多可以安全执行此类操作的情况很容易识别,并且这些情况符合“安全的低挂果”标准。然而,即使在可以执行此类优化的编译器上,什么时候应该或将会执行此项优化也不总是明显的。各种因素可能使尾调用的成本大于普通调用的成本,并且这些因素可能不总是可预测的。例如,如果一个函数以return foo(1,2,3,a,b,c,4,5,6);
结尾,则将a、b和c复制到寄存器中,清除堆栈并准备传递参数可能是可行的,但可能没有足够的寄存器来处理foo(a,b,c,d,e,f,g,h,i);
。
如果一种语言有一个特殊的“尾调用”语法,要求编译器在任何可能的情况下都进行尾调用,并拒绝编译否则,代码就可以安全地假设这样的函数可以任意嵌套。然而,在使用普通调用语法时,没有一般方法可以知道编译器是否能够更便宜地执行尾调用而不是“普通”调用。