C 语言中的尾调用优化相对较新(2025)
C 及相关语言中的尾调用优化,正从一种小众的编译器技巧,演变为开发者希望能依赖其正确性而不仅是性能的能力。评论者对比了那些在规范中保证尾调用的语言(如 Scheme、F#,以及 JavaScriptCore 对 JS 的实现)与 C、C++、Rust 和 C#——后者通常把 TCO 当作可选优化——因此有人提出了像 C++ 的 `[[musttail]]` 和 Rust 的 `become` 这样的方案,把 TCO 失败变成编译期错误。讨论还深入到调用约定、可变参数函数、析构/RAII 以及工具链约束为何让正确的尾调用难以实现,以及为什么它们对解释器、状态机和避免栈溢出的递归代码至关重要。
TCO 的范围与保证
- 很多人认为把尾调用优化(TCO)当作“只是一个优化”是有问题的,因为正确性(避免栈溢出、支持某些编程风格)可能依赖它。
- 对比那些在规范中强制要求 TCO 的语言(例如 Scheme)与 C/C++/JS:后者是可选的、依赖实现的。
- 一些人认为,如果没有语言级保证或“must tail”机制,依赖 TCO 就是不安全的。
属性、关键字与语言支持
- 现代 C/C++ 编译器(GCC/Clang/MSVC)提供
[[...::musttail]]属性:它们会尽力进行 TCO,如果做不到就报编译错误。 - 提议中的 Rust
become关键字:显式请求 TCO,先丢弃所有局部值,使调用处于真正的尾位置,并将 TCO 失败变成编译错误。 - 与 Scala 的
@tailrec(仅限自递归)以及 F# 在 .NET 上的 ILtail.指令进行比较;C# 并不可靠地产生这些指令。
C 标准化与历史
- C 本身至今仍没有保证的 TCO;当前支持仍是编译器特定扩展。
- 正在起草一个关于 C 尾调用和
defer的技术规范(TS);即使标准化之后,采用也会很慢,而且实现可以保持符合标准但拒绝该语法。 - 讨论提到 GCC 早期的一些限制(例如间接调用、可变参数),以及与其生命周期相比,可用的、通用的 TCO 在 GCC 中其实是相对较新的能力。
- MSVC 历史上在 C 和 TCO 支持方面较弱,近几年有所改善,但在 C 场景下仍被一些人持怀疑态度。
技术难点:调用约定与栈管理
- 可变参数函数和 ANSI C 之前的调用约定会使正确的尾调用变得复杂,因为只有调用者知道参数大小,并且必须清理栈。
- 调用者清理与被调用者清理的约定,以及跨模块调用,使得通用、可保证的 TCO 很难实现;函数指针和导出符号也会进一步限制编译器。
- 讨论中争论调用约定到底有多重要:当编译器同时控制调用者和被调用者时是否影响不大;另一些人则指出,独立编译和 ABI 稳定性会带来约束。
实践模式与风格争论
- TCO 不只是对阶乘这类递归循环重要;它对解释器、状态机、续延传递风格,以及互递归函数集合都很重要。
- 有些人更喜欢在 C 中显式使用循环;另一些人觉得尾递归形式更清晰,并认为 TCO 能带来更好的抽象,尤其是在更偏函数式的风格中。
- 还展示了用
goto手动实现的“ TCO ”,但与真正的递归加编译器支持相比,这种做法更容易出错,因此不被推荐。