為什么不用Rust?
最近我讀了一篇批評 Rust 的文章,雖然它提出了一堆好的觀點(diǎn),但我并不認(rèn)同它 -- 這是一篇容易引起爭論的文章。總的來說,我不會推薦一篇批評 Rust 的文章。這是一個(gè)遺憾 -- 正視缺點(diǎn)是很重要的,但也需要反對那些草率的或者不準(zhǔn)確失誤的批判。
所以,下面是我力挺 Rust 的一些理由。
不是所有的開發(fā)都是系統(tǒng)編程
Rust 是一種系統(tǒng)編程語言。它提供了對數(shù)據(jù)布局和代碼運(yùn)行時(shí)行為的精確控制,賦予你最大的性能和靈活性。與其他系統(tǒng)編程語言不同的是,它還提供了內(nèi)存安全--有bug的程序會以一種明確定義的方式終止,而不是出現(xiàn)(潛在的安全威脅)未定義的行為。
然而,在大多數(shù)情況下,人們并不需要終極性能或?qū)τ布Y源的極致控制。在這種情況下,像 Kotlin 或 Go 這樣的現(xiàn)代可管理語言提供了不錯(cuò)的速度和令人羨慕的性能,并且由于使用垃圾回收器進(jìn)行動態(tài)內(nèi)存管理而保證了內(nèi)存安全。
復(fù)雜度
程序員的時(shí)間是寶貴的,如果你選擇了 Rust,預(yù)計(jì)會有一部分時(shí)間花在學(xué)習(xí)各種使用技巧上。Rust 社區(qū)傾注了大量的時(shí)間來創(chuàng)建各種高質(zhì)量教程,但 Rust 語言很龐大。即使 Rust 能夠?yàn)槟闾峁﹥r(jià)值,你也可能沒有太多精力投入到語言專業(yè)知識的提升中。
Rust 提高控制力的代價(jià)是選擇的魔咒。
- struct Foo { bar: Bar }
- struct Foo<'a> { bar: &'a Bar }
- struct Foo<'a> { bar: &'a mut Bar }
- struct Foo { bar: Box<Bar> }
- struct Foo { bar: Rc<Bar> }
- struct Foo { bar: Arc<Bar> }
在 Kotlin 中,你開始 class Foo(val bar: Bar),就可以繼續(xù)解決你的業(yè)務(wù)問題了。在 Rust 中,你需要做出一些選擇,有些重要到需要專門的語法。
所有這些復(fù)雜性的存在是有原因的 -- 我們不知道如何創(chuàng)建一個(gè)更簡單的內(nèi)存安全的低級語言,雖然并不是每個(gè)任務(wù)都需要用低級語言來解決。
另請參見《為什么C++在瓦薩號沉沒時(shí)航行》。
https://www.youtube.com/watch?v=ltCgzYcpFUI
編譯時(shí)間
編譯時(shí)間是所有工作的倍數(shù)。用運(yùn)行速度較慢但編譯速度較快的編程語言編寫的代碼,可以有機(jī)會運(yùn)行得更快,因?yàn)槌绦騿T可以有更多的時(shí)間去優(yōu)化代碼。
Rust 在通用性的難題中有意挑選了慢速編譯器。這并不一定是世界末日(因?yàn)橛纱藥淼倪\(yùn)行時(shí)性能提升是實(shí)實(shí)在在的),但這確實(shí)也意味著在大型項(xiàng)目中,你將不得不為合理的構(gòu)建時(shí)間而拼盡全力。
rustc 實(shí)現(xiàn)了可能是生產(chǎn)型編譯器中最先進(jìn)的增量編譯算法,但這感覺有點(diǎn)像和語言編譯模型打架。
https://rustc-dev-guide.rust-lang.org/queries/incremental-compilation.html
與 C++ 不同的是,Rust 的構(gòu)建并不是尷尬的并行,并行的數(shù)量受限于依賴圖中關(guān)鍵路徑的長度。如果你有40多個(gè) core 進(jìn)行編譯,這就會體現(xiàn)出來。
Rust 還缺少一個(gè)類似于 pimpl 的功能,這意味著改變一個(gè) crate 需要重新編譯(而不僅僅是重新鏈接)其所有的反向依賴。
pimpl 見: https://en.cppreference.com/w/cpp/language/pimpl
成熟度
只有 5 歲,Rust 絕對是一門年輕的語言。盡管它的前景燦爛,但我曾經(jīng)在“C將在十年后存在”上下的賭注,要比“Rust 將在十年后存在”下的賭注多得多(參見 Lindy Effect)。如果你寫的軟件要持續(xù)幾十年,你應(yīng)該認(rèn)真考慮選擇新技術(shù)的相關(guān)風(fēng)險(xiǎn)。但請記住,90年代在銀行軟件上選擇 Java 而不是 Cobol,回想起來,證明是無比正確的選擇)。
Lindy Effect: https://en.wikipedia.org/wiki/Lindy_effect
Rust 目前僅有一個(gè)完整的實(shí)現(xiàn) -- rustc 編譯器。另一個(gè)最好的替代實(shí)現(xiàn) mrustc,故意省略了許多靜態(tài)安全檢查。rustc 目前只支持一個(gè)生產(chǎn)就緒的后端 -- LLVM。因此,它對 CPU 架構(gòu)的支持比 C 窄,C 架構(gòu)有 GCC 實(shí)現(xiàn),也有許多廠商特定的專有編譯器。
最后,Rust 缺乏官方規(guī)范。參考文檔是一個(gè)正在進(jìn)行中的工作,還沒有記錄所有細(xì)致的實(shí)現(xiàn)細(xì)節(jié)。
可替代性
在系統(tǒng)編程領(lǐng)域,除了 Rust 之外,還有一些其他語言,主要有 C、C++ 和 Ada。
現(xiàn)代 C++ 提供了提高安全性的工具和準(zhǔn)則。甚至有人提議建立類似 Rust 的生命周期機(jī)制。與 Rust 不同,使用這些工具并不能保證沒有內(nèi)存安全問題。然而,如果你已經(jīng)維護(hù)了大量的 C++ 代碼,那么檢查一下遵循最佳實(shí)踐和使用 sanitizer, 對于解決安全問題是有意義的。這很難,但顯然比用另一種語言重寫更容易。
如果你使用 C 語言,你可以使用形式化方法來證明沒有未定義的行為,否則你只能詳盡地測試一切。
Ada 是內(nèi)存安全的,如果你不使用動態(tài)內(nèi)存(永遠(yuǎn)不調(diào)用 free)。
Rust 是成本/安全曲線上的一個(gè)有趣的點(diǎn),但肯定不是唯一的一個(gè)點(diǎn)。
工具
Rust 工具是值得稱贊的東西?;€工具、編譯器和構(gòu)建系統(tǒng)(cargo),經(jīng)常被認(rèn)為是一流的。
但是,舉例來說,一些與運(yùn)行時(shí)相關(guān)的工具(最明顯的是堆分析)目前還不存在 -- 如果沒有運(yùn)行時(shí)工具,就很難對程序運(yùn)行時(shí)進(jìn)行分析。此外,雖然 IDE 的支持還算不錯(cuò),但遠(yuǎn)沒有達(dá)到 Java 級別的可靠性。如今在 Rust 中,數(shù)百萬行程序的自動復(fù)雜重構(gòu)還做不到。
集成
不管 Rust 的愿景是什么,今天的系統(tǒng)編程世界還是 C 和 C++ 的天下,這是一個(gè)事實(shí)。Rust 有意不試圖模仿這些語言 —— 它沒有使用 C++ 風(fēng)格的類或 C ABI。
這意味著,它們之間的集成需要明確的橋梁。這些都不是無縫的。它們是不安全的,并不總是零成本的,并且需要在語言之間同步。雖然片斷式集成的一般也還能維持,工具也能趕上,但一路上卻意外的復(fù)雜。
一個(gè)具體的小麻煩是,Cargo 獨(dú)特的世界觀(這對純 Rust 項(xiàng)目來說是個(gè)福音)可能會使它更難與更大的構(gòu)建系統(tǒng)集成。
性能
"使用LLVM" 并不是解決所有性能問題的通用方案。雖然我不知道 C++ 和 Rust 在規(guī)模上的性能的基準(zhǔn),但不難列出一個(gè) Rust 不如 C++ 一些性能問題列表。
最大的一個(gè)可能是 Rust 的 move 語義是基于 value 的(機(jī)器代碼級別的 memcpy)。相比之下,C++ 語義使用的是你可以使用數(shù)據(jù)的特殊引用(機(jī)器代碼級的指針)。理論上,編譯器應(yīng)該能夠看穿復(fù)制鏈,但在實(shí)踐中往往不能。#57077. 一個(gè)相關(guān)的問題是沒有放置 new -- Rust 有時(shí)需要從堆棧中復(fù)制字節(jié),而 C++ 可以在原地構(gòu)造東西。
57077 https://github.com/rust-lang/rust/issues/57077
有點(diǎn)有趣的是,Rust 的默認(rèn) ABI(為了使其盡可能高效,它并不穩(wěn)定)有時(shí)比 C 更差。#26494.
https://github.com/rust-lang/rust/issues/26494#issuecomment-619506345
最后,雖然理論上 Rust 代碼應(yīng)該更高效,因?yàn)橛懈S富的別名信息,但啟用別名相關(guān)的優(yōu)化會引發(fā) LLVM bug 和誤編譯。#54878.
https://github.com/rust-lang/rust/issues/54878
但是,重申一下,這些都是挑出來的例子,有時(shí)候這些領(lǐng)域會有相反的情況。例如,std::unique_ptr 的性能問題在 Rust 的 Box 中就不存在。
一個(gè)潛在的更大的問題是,Rust 的定義時(shí)間檢查的泛型,表現(xiàn)力不如 C++。所以,一些高性能的 C++ 模板技巧,在 Rust 中就很難用漂亮的語法來表達(dá)。
模板技巧 http://eigen.tuxfamily.org/index.php?title=Expression_templates
Unsafe 的定義
一個(gè)比所有權(quán)和借用更核心的問題也許是 unsafe 的邊界。通過在 unsafe 的塊和函數(shù)后面劃定所有危險(xiǎn)的操作,并為它們提供安全的上層接口,就有可能創(chuàng)建一個(gè)既是:
- 合理的(非不安全的代碼不能導(dǎo)致未定義行為)。
- 和模塊化 (不同的不安全塊可以分別檢查)。
很明顯,這個(gè)承諾在實(shí)踐中得到了驗(yàn)證:有問題的 Rust 代碼會帶來 panic,而不是緩沖區(qū)超限。
但在理論上來看,問題就不那么樂觀了。
首先,沒有 Rust 內(nèi)存模型的定義,所以無法正式檢查給定的不安全塊是否有效。有非正式的 "rustc 所做的或可能依賴的事情 "的定義,并且在進(jìn)行中的運(yùn)行時(shí)驗(yàn)證器,但實(shí)際的模型是在變化的。所以可能有一些不安全的代碼,今天在實(shí)踐中還可以用,明天可能就被宣布無效,明年又被新的編譯器優(yōu)化打破。
其次,還有一個(gè)觀察,不安全塊其實(shí)不是模塊化的。足夠強(qiáng)大的不安全塊實(shí)際上可以擴(kuò)展語言。兩種這樣的擴(kuò)展單獨(dú)使用可能是好的,但如果同時(shí)使用會導(dǎo)致未定義的行為,觀測等價(jià)性和不安全代碼。
見: https://smallcultfollowing.com/babysteps/blog/2016/10/02/observational-equivalence-and-unsafe-code/
最后,編譯器中存在 bug。
見:https://github.com/rust-lang/rust/issues?q=is%3Aopen+is%3Aissue+label%3A%22I-unsound+%F0%9F%92%A5%22
下面是我刻意忽略的一些東西。
- 經(jīng)濟(jì)學(xué):("雇用 Rust 程序員很難了")--我覺得 "成熟度 "部分抓住了它的本質(zhì),它不能還原成雞和蛋的問題。
- 依賴性:("stdlib太小/所有東西都有太多的依賴")-- 考慮到 Cargo 和語言的相關(guān)部分有多好,我個(gè)人不認(rèn)為這是一個(gè)問題。
- 動態(tài)鏈接:("Rust 應(yīng)該有穩(wěn)定的ABI")--我不認(rèn)為這是一個(gè)強(qiáng)有力的論點(diǎn)。單態(tài)化與動態(tài)鏈接在根本上是很不兼容的,如果真的有需要,還有 C ABI 可用。我確實(shí)也認(rèn)為這里有可改善空間,但我不認(rèn)為這種改善需要針對 Rust。見:https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347/10?u=matklad
英文原文:
https://matklad.github.io/2020/09/20/why-not-rust.html
本文轉(zhuǎn)載自微信公眾號「高可用架構(gòu)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系高可用架構(gòu)公眾號。