Gforth核心开发者Anton Ertl在LWN评论区甩出一个反常识判断:C语言对解释器分派所需尾调用的可靠支持,不是编译器诞生时就有的老功能。它真正成熟,是最近几年的事。这条评论挂在LWN一篇讨论Python尾调用性能的文章下面,信息量比原文本身还大——它把一项Python社区已经用起来的编译器能力,往前倒查了三十年。
Ertl的核心判断很直接:真正值得关心的不是"尾调用优化是不是新鲜事",而是编译器能力补齐之后,解释器能不能摆脱computed goto的代码规模限制。这对解释器、虚拟机和编译器开发者是个提醒——旧的"C做不了间接尾调用"的经验可能已经过时,但具体多大范围内成立,还没人系统验证过。
从1994年不支持到现在能用:尾调用优化走了近三十年
C的传统调用约定里,很多实现由调用者负责清理栈上的参数。这一步通常会在call和return之间插入清理操作,一旦存在,这次调用就不再是纯粹的尾调用,编译器也没法把它优化成一次跳转。这是常见惯例,不是所有ABI的强制规则。
Ertl说他在1994年查过当时的C编译器,做不到文章里展示的那种尾调用优化。2001年,Mark Probst在论文里为GCC实现了一套独立调用约定来支持尾调用。但Probst自己列出的限制很明确:这套方案处理不了间接调用。
间接调用恰恰是解释器做指令分派最常用的手法。这项优化诞生二十多年,却在最需要它的场景里一直缺一块。Ertl后来没再细究,因为GCC提供的computed goto已经够用。直到去年读到Xu和Kjolstad那篇讲Copy-and-Patch Compilation的论文,他才重新测了现在的GCC和Clang——发现文章里展示的那类尾调用,两家编译器现在都能处理。
这里有个边界要说清楚:Ertl测的是他文章里展示的那种特定尾调用写法。不是所有架构、所有优化级别、所有调用形式都保证能被优化成跳转。
Python已经在新解释器里用上,这是分派机制,不是语言尾递归
Python生态走在了Gforth前面。CPython团队在新版本里尝试引入一套实验性的尾调用解释器,用尾调用做字节码指令分派,替代传统的switch-case或computed goto分派。这套方案依赖编译器支持musttail一类的强制尾调用属性,目前主要靠新版Clang启用。
这里要分清一个容易被搞混的点:这是CPython解释器内部的分派实现换了写法。不是Python语言给开发者提供了通用的尾递归优化——写Python代码递归调用自己,不会因为这项改动就不再爆栈。受影响的是CPython自己的C源码,不是用户写的Python脚本。
关注Python执行性能的技术决策者,现在能做的判断有限:这更像一个内部工程选择,还没到公开性能实测数据、可以直接拿来评估收益的阶段。原始评论里也没有给出具体版本号和构建配置,想核实的人需要自己去查CPython的构建文档。
Gforth还没跟,代码片段规模是关键限制
Gforth用computed goto做虚拟机指令分派,为了控制代码体积,把可用的代码片段数量限制在不到2000个。Xu和Kjolstad的Copy-and-Patch论文里,用到了10万个代码片段。
| 项目 | 当前状态 | 限制/备注 |
|---|---|---|
| Gforth | 仍用computed goto分派 | 代码片段数被压在2000以内,尾调用方案还没采用 |
| GCC / Clang | Ertl实测可处理文中展示的尾调用写法 | 未系统验证所有架构和优化级别 |
| CPython实验性解释器 | 已尝试用尾调用做分派 | 依赖musttail等编译器支持,属于分派实现 |
| Copy-and-Patch方案 | 论文中用到10万个代码片段 | 与Gforth不是同一套体系,不能直接换算成性能倍数 |
两者的对比不是同一套体系下的性能测试,而是分派机制能承受多大代码规模的差别。Ertl的判断是:如果尾调用优化足够可靠,解释器就不必再被computed goto的规模天花板卡住,可以用上那些需要生成大量不同代码片段的技术——这些技术在goto *体系里之前根本玩不起。
Gforth这样的项目,现实选择是:先在测试分支里验证尾调用在自己关心的架构和优化级别下是否稳定,再决定要不要迁移分派机制,而不是马上重写。其他还在用computed goto的解释器项目,更该做的是重新测一遍这条老经验是否还成立,而不是照抄一个方案。
接下来最该盯的变量,是Gforth会不会真的把尾调用用进代码,以及CPython之外还有没有别的运行时跟进——这两件事目前都没有时间表。
