【翻譯】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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索