【翻译】Rust中的尾递归优化的故事

原文标题: The Story of Tail Call Optimizations in Rusthtml

原文连接: https://dev.to/seanchen1991/the-story-of-tail-call-optimizations-in-rust-35hfpython

公众号:Rust碎碎念git

我认为尾调用优化(tail call optimizations)至关整洁,特别是它们解决递归函数如何调用这类基本问题的方式。诸如Haskell和Lisp家族这类函数式语言,以及逻辑语言(Prolog多是最著名的例子)都强调采用递归的方式思考问题。这些语言经过尾调用优化能够在性能上得到许多好处。
程序员

注意: 我不会在这篇文章里解释尾调用的概念。下面是一些比较好的相关资料:es6

  1. Youtube频道 Computerphile [1] 有一个 视频 [2],详细讲解了尾递归函数的示例。
  2. StackOverflow [3]上有个关于尾递归概念的详细解释。

随着最近几年编程社区强调函数范式和函数式风格的趋势,您可能会认为尾调用优化已经出如今许多编译器/解释器的实现中。然而,事实上不少这类流行语言并无实现尾调用优化。Javascript在几年前还支持,可是后来将其移除[4]Python不支持[5],Rust也不支持。

在深刻探究为何会这样以前,让咱们简要地总结一下尾调用优化背后的思想。github

尾调用优化是如何工做的(理论上)

尾递归函数,若是运行在一个不支持TCO(译者注:TCO==Tail Call Optimization, 即尾调用优化)的环境中,会出现内存随着函数输入的大小而线性增加的状况。这是由于每一个递归调用都会向调用栈分配一个额外的栈帧。TCO的目标就是经过一种不须要为每一个调用分配栈帧的方式运行尾递归函数来消除这种线性内存占用。

一种实现方式就是让编译器来作这件事,一旦编译器发现须要执行TCO,就把尾递归函数执行转换成一个迭代循环。这意味着尾递归函数的结果只须要占用单个栈帧就能计算出来。内存使用为常量。web

有了上面这些知识,让咱们回来看看,为何Rust没有作TCO。编程

回顾Rust的时光机

我能找到的最先关于Rust中尾调用优化的相关资料,能够追溯到Rust项目的开始阶段。我发现了来自2013年的这些邮件列表[6],在这些邮件列表中,Graydon Hoare详细列出了关于为何他认为尾调用优化不属于Rust的观点。微信

这份邮件列表是来自大约2011年的GitHub上的这个[7]issue, 当时这个项目的几位初始成员正在思考如何在后来崭露头角的编译器上实现TCO。当时问题的核心彷佛是因为LLVM的不兼容;说实话,他们讨论的不少东西我都没法理解。

有趣的是,尽管有了最初关于TCO不会在Rust中实现(也是来自最初的做者,毫无疑问)的悲观预测,时至今日,人们仍然没有放弃尝试在rustc中实现TCO。架构

在rustc中添加TCO的后续提议

在2014年五月,这个[8]PR被开启,其中提到,关于早期邮件列表里提到的问题,LLVM如今已经可以支持TCO了。更具体地说,这个PR旨在经过引入一个名为become的新关键字来启用按需TCO( on-demand TCO)。

在这个PR生命周期的整个过程当中,有人指出rustc可以,在特定状况下,推断出何时TCO是合适的而且执行它[9]。所以,被提议的become关键字和unsafe相似,只是专门适用于TCO。

接下来的一个RFC在2017年2月份开启,和以前的提议很是类似。有趣的是,这个RFC做者提出,实现尾调用优化(也被称为"正确尾调用(proper tail calls)")的一些最大障碍能够归结以下:

  • 可移植性问题;LLVM当时在某些指定架构上特别是MIPS和WebAssembly,不支持正确尾调用。
  • LLVM中正确尾调用实际上可能会因为它们当时的实现方式而形成性能损失。
  • TCO让调试变得更加困难,由于它重写了栈上的值。

的确,RFC的做者认可,到目前为止,在没有TCO的状况下,Rust运行得很是好,并且会一直很是好。

目前为止,显式地由用户控制的TCO尚未加入到rustc。

经过一个库启用TCO

尽管如此,许多阻碍TCO相关的RFC和提议的问题能够在必定程度上获得避免。出现了几个添加TCO到Rust里的自制解决方案。

这些方案的共同思想是实现一个成为"trampoline"的东西。这指的是实际使用迭代循环来替代尾递归函数的抽象。

咱们先用一个trampoline实现它,做为一个缓慢的跨平台回退实现,而后依次为每一个架构/平台实现更快的方法,怎么样?

经过这种方式,该特性能够很是迅速地准备好,以便人们可使用它进行优雅的编程。在rustc的将来版本中,这样的代码将神奇地变得更快。

@ConnyOnny[10]

Bruno Corrêa Zimmermann’s的tramp.rs[11]库多是这些库解决方案里知名度最高的一个。让咱们在下面来看一下它是如何工做的。

深刻tramp.rs

tramp.rs库导出了两个宏, rec_call!rec_ret!,这和前面提到的become关键字同样改进了相同的行为:它容许程序员经过迭代循环提示Rust运行时执行指定的尾递归函数,从而将函数的内存开销下降到一个常数级别。

rec_call!这个宏启动了这个过程,若是这个关键字被引入到rustc里的话,也是和become关键字最类似的。

macro_rules! rec_call {
   ($call:expr) => {
       return BorrowRec::Call(Thunk::new(move || $call));
   };
}

rec_call!利用了额外的两个重要的概念,BorrowRecThunk

enum BorrowRec<'a, T> {
    Ret(T),
    Call(Thunk<'a, BorrowRec<'a, T>>),
}

BorrowRec枚举表示一个尾递归函数调用在任意时刻可能处于的两种状态: 要么它尚未到达基础状态(base case),也就是咱们仍然处于BorrowRec::Call状态,或者它已经达到了一个基础状态而且产生了它最终的值,这种状况下被认为是达到了BorrowRec::Ret状态。

BorrowRec枚举的Call变量包含下面这个Thunk的定义:

struct Thunk<'a, T> {
    fun: Box<FnThunk<Out = T> + 'a>,
}

Thunk结构体持有一个对尾递归函数的引用,这个尾递归函数由FnThunk这个trait来表示。

最后,这些都经过tramp函数联系在一块儿:

fn tramp<'a, T>(mut res: BorrowRec<'a, T>) -> T {
    loop {
        match res {
            BorrowRec::Ret(x) => break x,
            BorrowRec::Call(thunk) => res = thunk.compute(),
        }
    }
}

它接收一个包含尾递归函数的BorrowRec实例做为输入,而且只要BorrowRec停留在Call状态就一直调用这个函数。另外,当递归函数到达带有最终计算出的值的Ret状态时,最终的值会经过rec_ret!宏来返回。

这是TCO吗?

因此,这样对吗?tramp.rs是咱们须要来在Rust编程中启用按需TCO的英雄,对么?

恐怕不是这样。

虽然我很喜欢这个实现中使用trampolining做为一种增量引入TCO的方式,@timthelion[12]已经完成的性能测试[13]代表,相较于手动把尾递归函数转换成迭代循环,使用tramp.rs会致使一个轻微的性能回退。

致使tramp.rs性能降低的部分缘由多是,正如@jonhoo指出的,每一个rec_call!调用了Thunk::new,而致使在堆上分配内存。

因此这说明,tramp.rs的trampolining实现甚至没有达到以前TCO承诺的常量内存使用。

也许按需TCO未来会被添加到rustc中,也许不会。目前为止,即便没有TCO,也能过得很好。

参考资料

[1]

Computerphile: https://dev.tocomputerphile/

[2]

视频: https://youtu.be/_JtPhF8MshA

[3]

StackOverflow: https://stackoverflow.com/questions/310974/what-is-tail-call-optimization

[4]

Javascript移除尾调用: https://stackoverflow.com/questions/42788139/es6-tail-recursion-optimisation-stack-overflow

[5]

Python不支持尾调用: http://neopythonic.blogspot.com/2009/04/final-words-on-tail-calls.html

[6]

这些邮件列表: https://mail.mozilla.org/pipermail/rust-dev/2013-April/003557.html

[7]

这个: https://github.com/rust-lang/rust/issues/217

[8]

这个: https://github.com/rust-lang/rfcs/pull/81

[9]

推断出何时TCO是合适的而且执行它: https://github.com/rust-lang/rfcs/issues/271#issuecomment-271161622

[10]

ConnyOnny: https://github.com/connyonny

[11]

tramp.rs: https://crates.io/crates/tramp

[12]

timthelion: https://github.com/timthelion

[13]

性能测试: https://gitlab.com/timthelion/trampoline-rs/commit/84f6c843658c6c3a5893effa031ce734b910171c



本文分享自微信公众号 - Rust语言中文社区(rust-china)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索