返回资讯中心
外部精选
软件工程
#开发者实时流

开发者提议为 LLVM 和 Clang 新增 tail_chaincc 尾调用约定

来源标题:RFC tail_chaincc – A Maximum-Efficiency Tail Call Convention

开发者向 LLVM 和 Clang 提议新增名为 tail_chaincc 的调用约定,用于配合 C/C++ 的 [[clang::musttail]] 实现高效保证尾调用。该约定结合 tailcc 的被调用方清理机制与 preserve_none 的寄存器优化,兼顾易用性与性能。提议者希望它成为 musttail 用户的首选调用约定,并为语言层面更完整支持保证尾调用铺路。

Hacker News 最新dcminter2026/11/3 · 今天30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

我提议为 LLVM 和 Clang 增加一种新的调用约定(暂定名为 tail_chaincc)。这种新约定旨在与 C 和 C++ 中的 [[clang::musttail]](以及任何其他希望通过 LLVM 使用保证尾调用的语言,例如 Rust 中的 become)配合使用,其设计目的是解决我们在使用尾调用时遇到的若干实际问题。

这种新约定将 tailcc 的被调用方清理机制与 preserve_none 的寄存器优化相结合,从而带来既符合人体工程学又高性能的体验。我预计它将成为几乎所有 [[clang::musttail]] 用户的首选调用约定。

我希望这将有助于为语言实现和标准更全面地支持保证尾调用铺平道路,解决我们如今在使用尾调用时遇到的一些注意事项和头痛问题。

注意:如果可以接受破坏现有 tailcc 约定的 ABI 兼容性,我们可以将这里描述的增强功能添加到现有约定中,而不是新增一个约定。

自 2021 年以来,Clang 一直支持 [[clang::musttail]] 属性以实现保证尾调用。这一 Clang 特性依托于 2014 年添加到 LLVM IR 中的既有 musttail 支持。

我此前之所以有动力为 Clang 添加 musttail 支持,是因为我和一位同事发现,使用尾调用链解析 Protobuf 的性能优于我们尝试过的任何其他技术,这很大程度上是因为它帮助编译器生成了更优的寄存器分配序列。我在 2021 年的博客文章《以 2+GB/s 解析 Protobuf:我如何学会爱上 C 中的尾调用》中记录了我们的结果,并指出该技术可推广到编程语言解释器(以及任何具有类似形态的代码)。

自那以后,人们对尾调用的兴趣逐渐扩大。最值得注意的是,CPython 在 3.14 中发布了一个实验性的尾调用解释器。这反过来又促使编译器为 musttail 提供更多支持:

标准机构也表现出了兴趣:

尽管 [[clang::musttail]] 是一个强大的工具,但在与现有调用约定一起使用时,它会遇到一些严重问题。

当 LLVM 在 2014 年为 IR 添加 musttail 支持时,它附带了一个约束:调用方和被调用方必须具有相同的函数签名。添加此功能的 Reid Kleckner(@rnk)在他的博客文章《关于放宽 Musttail 原型要求》中给出了一些关于为何如此设计的历史。概括来说,其意图有两方面:

这两个目标至少部分实现了。我们可以通过比较 Clang 与 GCC 来观察这一点,后者也添加了 musttail 支持,但没有这条规则。考虑以下示例:

int f(int a); int g() { [[clang::musttail]] return f(1);

Clang 在前端以不匹配原型规则为由拒绝此代码。GCC 在前端允许它,并且在通过寄存器传递 a 的架构上编译完全成功。但在 x86(32 位)上,编译在后端失败,报错如下:

<source>: In function 'int g()': <source>:5:33: error: cannot tail-call: callee required more stack slots than the caller 5 | [[clang::musttail]] return f(1);

这正是 Clang 的规则旨在防止的那类不可移植性。

GCC 也处理过类似 #118430 的缺陷,其中优化遍以破坏尾调用的方式重写代码,从而引入破坏。如果没有关于何时允许尾调用的明确规则,这类破坏必须逐案处理。

尽管这条规则显然带来了一些好处,但它也导致了许多不良后果,同时也没有完全提供预期的可靠性/可移植性。

第一个也是最直接的问题是人体工程学。匹配原型规则本质上是保守的:它以可移植性的名义在所有架构上强加了一条一刀切的规则。

在实践中,这条规则在许多架构上比必要的更具限制性。考虑这样一个函数:

int g(); int f(int x) { // 在大多数架构上可以轻松尾调用(因为 // x 将通过寄存器传递)。 [[clang::musttail]] return g();

在任何通过寄存器传递参数的平台上,g() 都可以轻松进行尾调用优化,即使使用调用方清理约定,因为参数不需要任何栈空间。

Clang 因调用方/被调用方原型不匹配而拒绝上述代码,这导致了许多开发者的抱怨,尤其是当人们可以清楚地观察到,在省略 musttail 时给定的尾调用是可以执行的。

GCC 允许上述不可移植的代码,并且仅在尾调用在当前平台上无法降低时才会报错。这更灵活,但也更不可预测。它还导致 Clang 和 GCC 之间的语义不一致。

匹配原型规则的设计初衷是以灵活性换取可预测性和可移植性。在实践中,它并未兑现所承诺的可移植性:自发布 [[clang::musttail]] 以来,我们收到了许多后端崩溃的报告,甚至是在遵循匹配原型规则的规范程序中也是如此(例如 #53271、#84090、#50758、#161193)。

与其他问题不同,这个问题如果有足够的时间和注意力,或许可以解决。但所提议的约定可能更容易、更系统地解决它,同时还能改善人体工程学和性能。

标准调用约定的寄存器分配对于尾调用链来说并非最优。许多(但并非全部)这些问题此前已在 preserve_none 调用约定中得到解决。这些问题将在下面的提案部分详细讨论。

LLVM 已有一种使用被调用方清理设计的现有调用约定 tailcc。正如 Eli Friedman(@efriedma-quic)在早先的讨论中所指出的,由于 tailcc 设计为在所有情况下都允许尾调用,它在 LLVM IR 中被特殊处理,以便即使调用方和被调用方函数签名不匹配也允许 musttail。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →