为什么尾部调用优化需要操作码

本文关键字:操作码 优化 尾部 调用 为什么 | 更新日期: 2023-09-27 18:31:17

所以我之前读过很多次,从技术上讲,.NET 确实支持尾部调用优化 (TCO),因为它具有操作码,而 C# 不会生成它。

我不确定为什么TCO需要操作码或它会做什么。据我所知,能够执行TCO的要求是递归调用的结果不与当前函数范围中的任何变量组合。如果你没有这个,那么我不明白操作码如何防止你不得不保持堆栈帧打开。如果你确实有,那么编译器不能总是轻松地将其编译为迭代的东西吗?

那么操作码的意义何在呢?显然,我缺少一些东西。在可能实现 TCO 的情况下,难道不能总是在编译器级别而不是在操作码级别处理它吗?它不能的地方有什么例子?

为什么尾部调用优化需要操作码

按照您已经提供的链接,在我看来,这是非常密切地回答您的问题的部分。

<小时 />

CLR 和尾部调用

当您处理由 CLR 管理的语言时,有两种编译器在起作用。有一个编译器,从语言的源代码到IL(C#开发人员将其称为csc.exe),然后是从IL到本机代码的编译器(在运行时或NGEN时调用的JIT 32/64位编译器)。源>IL 和 IL>本机编译器都了解尾部调用优化。但是 IL->native 编译器(我只将其称为 JIT)对最终是否使用尾调用优化拥有最终决定权。源>IL 编译器可以帮助生成有利于进行尾部调用的 IL,包括使用"尾部"。 IL 前缀(稍后会详细介绍)。这样,源>IL 编译器可以构建它生成的 IL,以说服 JIT 进行尾部调用。但是JIT总是可以选择做任何它想做的事情。

JIT 何时进行尾部调用?

问了陈飞和格兰特·里金斯,他们是我大厅的邻居,他们碰巧在JIT上工作,各种JIT将在什么条件下采用尾部调用优化。完整的答案相当详细。快速总结是,JIT 尽可能尝试使用尾部调用优化,但无法使用尾部调用优化的原因有很多。尾部调用是不可选择的一些原因:

    呼叫
  • 者在呼叫后不会立即返回(duh :-))
  • 调用方和被调用方
  • 之间的堆栈参数不兼容,需要在被调用方的帧中移动内容,然后才能执行
  • 调用方和被叫方返回不同的类型
  • 我们改为内联调用(内联比尾部调用好得多,并为更多优化打开了大门)
  • 安全成为障碍
  • 调试器/探查器关闭了 JIT 优化
<小时 />

在您的问题上下文中最有趣的部分,在我看来非常清楚,在许多场景中,是上面提到的安全性示例......

在许多情况下,.NET 中的安全性取决于堆栈的准确性...在运行时..这就是为什么,如上所述,负担由源代码到 CIL 编译器和(运行时)CIL 到本机 JIT 编译器分担,最终决定权在后者。

猜猜:在像 x86 汇编程序这样"手动"管理堆栈的简单语言中,您不需要操作码 - 您可以适当地设置调用堆栈。

但是在像 .NET CIL 这样更高级别的东西中,堆栈是为您部分管理的,并且调用函数的整个行为是单个操作码(例如调用)。因此,您需要一个不同的操作码来实现 TCO——一个"将控制流传递给此函数,但不创建新的堆栈帧"的操作码。