我爲什麼反對使用Rust?

本文最初發佈於matklad的個人博客,由InfoQ中文站翻譯並分享。

我最近讀到一篇批評Rust的文章,雖然它提出了一堆好觀點,但我不喜歡它——這篇文章很容易引起爭論。一般來說,我覺得我不能推薦一篇批評Rust的文章。這是件有點令人丟臉的事——直面缺點很重要,而且,批評批評者不努力或知識欠缺會錯過一些實際上很好的論點。

以下是我反對Rust的理由。

不是所有的編程都是系統編程

Rust是一種系統編程語言。它可以精確控制數據佈局和代碼的運行時行爲,爲你提供了最大的性能和靈活性。與其他系統編程語言不同,它還提供了內存安全性——有Bug的程序會以定義良好的方式終止,而不是觸發未定義的行爲(可能是安全敏感的行爲)。

然而,在許多(大多數)情況下,我們不需要最好的性能或對硬件資源的控制。在這種情況下,像Kotlin或Go這樣的現代化託管語言提供了不錯的速度、令人羨慕的執行時間,並且由於使用垃圾收集器進行動態內存管理而具有內存安全性。

複雜性

程序員的時間非常寶貴,如果你選擇Rust,那麼你得準備好花一些時間來學習一些技巧。Rust社區投入了大量的時間來編寫高質量的教程,但是Rust語言非常大。即使Rust實現能夠爲你提供價值,你也可能無法投入資源來提高這門語言的專業技能。

Rust爲改善控制所付出的代價是選擇麻煩:

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),然後繼續解決你的業務問題。在Rust中,你需要做出一些選擇,其中一些非常重要,以至於需要專門的語法。

所有這些複雜性的存在都是有原因的——我們不知道如何創建一個更簡單的內存安全的低級語言。但並不是所有的任務都需要低級語言來解決。

延伸閱讀:爲什麼C++避免了“瓦薩”號沉沒的命運?(https://www.youtube.com/watch?v=ltCgzYcpFUI)

編譯時間

編譯時間是所有事情的倍增器。如果一門語言運行速度慢但編譯速度快,那麼用它編寫的程序也可以運行得更快,因爲程序員將有更多的時間來做優化!

Rust在泛型困境中有意選擇了慢速編譯器。這並不是世界末日(最終的運行時性能改進是真的),但它確實意味着,在較大的項目中,你必須竭盡全力才能獲得合理的構建時間。

rustc實現了可能是生產編譯器中最先進的增量編譯算法,但這感覺有點像是在與語言編譯模型作鬥爭。

與C++不同,Rust的構建並不是並行的;並行量受依賴圖中關鍵路徑的長度限制。如果你有40多個core需要編譯,這就顯出來了。

Rust還缺乏與pimpl慣用語類似的東西,這意味着更改一個crate需要重新編譯(而不僅僅是重新鏈接)其所有反向依賴關係。

成熟度

Rust才五歲,絕對是一門年輕的語言。儘管它的未來看起來很光明,我還是會把更多的錢押在“C語言10年後仍將存在”上,而不是“Rust語言10年後仍將存在”上(參見林迪效應)。如果你編寫的軟件要用幾十年,那麼你應該認真考慮選擇新技術伴隨的風險。(但是請記住,在90年代,爲銀行軟件選擇Java而不是Cobol是正確的選擇)。

Rust只有一個完整的實現——rustc編譯器。最先進的替代實現mrustc有意省略了許多靜態安全檢查。目前,rustc只支持一個可用於生產的後端——LLVM。因此,它對CPU架構的支持比C更窄,C有GCC實現和許多特定於供應商的專有編譯器。

最後,Rust缺乏官方規範。語言參考還在編寫中,文檔中也沒有詳細提供所有的實現細節。

替代語言

在系統編程領域,除了Rust外,還有其他語言,比較出名的有C、C++和Ada。

現代C++爲提高安全性提供了工具指南。甚至有人提議,建立一個類似Rustlifetime的機制

!與Rust不同,使用這些工具並不能保證不出現內存安全問題。但是,如果你已經維護了大量的C++代碼,那麼還是檢查下,這些代碼是否遵循了最佳實踐,並藉助sanitizers檢測安全問題。這很難,但顯然比用另一種語言重寫要容易得多!

如果你使用C,則可以使用正式的方法來證明沒有未定義的行爲,或者只是詳盡地測試一切。

Ada是內存安全的,如果你不使用動態內存(永遠不調用free)。

Rust是成本/安全曲線上一個有趣的點,但不是唯一的點!

工 具

Rust工具有好有壞。基線工具、編譯器和構建系統(cargo),經常被認爲是最好的工具。

但是,舉例來說,它缺少一些運行時相關的工具(最明顯的是堆分析)——如果沒有運行時,就很難反映程序的運行時狀況!此外,儘管IDE支持很不錯,但它的可靠性還遠遠達不到Java級別。在如今的Rust中,自動對數百萬行程序進行復雜的重構是不可能的。

集 成

無論Rust的承諾是什麼,如今的系統編程世界都是使用C語言,由C和C++所佔據,這就是事實。Rust有意避免模仿這些語言——它不使用C++風格的類或C ABI。

這意味着,世界之間的整合需要明確的橋樑。這不是無縫的。它們unsafe,總會有點成本,並且需要在語言之間保持同步。雖然分段集成的承諾已經實現,工具也趕上了進度,但在此過程中會出現偶發的複雜性。

一個特別的問題是,Cargo固執的世界觀(這對於純Rust項目來說是件好事),但可能會使它更難與更大的構建系統集成。

性 能

“使用LLVM”並不是所有性能問題的通用解決方案。雖然我不知道在規模較大時,C++和Rust基準測試的性能對比,但是不難想出一組場景,在這些場景中,相對於C++,Rust還有一些性能問題沒有解決。

最大的問題可能是,Rust的move語義是基於值的(機器代碼級的memcpy)。相反,C++語義使用特殊的引用,你可以從中竊取數據(機器代碼級的指針)。理論上,編譯器應該能夠識別副本鏈;實踐中,情況通常並非如此:#57077。一個相關的問題是,沒有placement new——Rust有時需要從棧複製字節/將字節複製到棧,而C++可以在適當的位置構造。

有點可笑的是,Rust的默認ABI(爲使其儘可能高效而不穩定)有時比C更糟糕:#26494

最後,雖然從理論上講,由於Rust具有豐富得多的aliasing信息,其代碼應該更高效,但啓用與aliasing相關的優化將觸發LLVM Bug和編譯錯誤:#54878

但是,重申一下,這些都是刻意挑選的例子,可能有失偏頗。例如,std::unique_ptr有一個性能問題,而Rust的Box則沒有。

一個潛在的更大的問題是,Rust的定義時檢查泛型比C++表達性差。因此,一些用於提高性能的C++模板技巧無法在Rust中使用好的語法進行表達。

不安全的含義

對於Rust而言,一個比ownership&borrowing更核心的概念可能是unsafe邊界。將所有的危險操作都放在unsafe塊和函數的後面,併爲它們提供一個安全的高層接口,就有可能創建一個同時滿足以下兩點的系統:

  1. 健壯(非unsafe代碼不會導致未定義的行爲);

  2. 模塊化(不同的unsafe塊可以單獨檢查)。

很明顯,在實踐中,這種保證是有效的:對Rust代碼進行模糊測試,發現隱藏的panics,避免緩衝區溢出。

但理論上,前景並不樂觀。

首先,缺少Rust內存模型定義,就不可能正式檢查給定的不安全塊是否有效。對於“rustc所做的或可能依賴的事情”和正在開發中的運行時驗證器,都只有非正式的定義,而實際的模型是不斷變化的。因此,某些地方可能存在一些unsafe代碼,這些代碼今天還能正常工作,明天就可能被聲明失效,並在明年被新的編譯器優化破壞掉。

其次,還有一種觀點認爲,unsafe塊實際上並不是模塊化的。總之,功能足夠強大的unsafe塊可以擴展該語言。兩個這樣的擴展單獨使用可能沒有問題,但如果同時使用,則會導致未定義的行爲:觀測性等價和不安全代碼

最後,編譯器中還有公開的Bug

下面是我特意從清單中刪除的一些內容:

  • 經濟因素(“招聘Rust程序員更困難”)——我覺得“成熟度”一節抓住了問題的本質,它不能簡化爲雞和蛋的問題。

  • 依賴關係(“stdlib太小/所有東西都有太多的依賴”)——考慮到Cargo以及語言的相關部分都很不錯,我個人並不認爲這是一個問題。

  • 動態鏈接(“Rust應該有穩定的ABI”)——我不認爲這是一個強有力的論點。從根本上來說,單態化(Monomorphization)與動態鏈接是不兼容的,如果需要的話,還有C ABI。我確實認爲這裏的情況可以改善,但我不認爲這種改善是Rust特有的

感興趣的讀者可以點擊這裏:https://www.reddit.com/r/rust/comments/iwij5i/blog_post_why_not_rust/ ,查看關於本文的討論。

查看英文原文:

https://matklad.github.io/2020/09/20/why-not-rust.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章