天天看點

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

作者:Kevin Wang

前幾天,我發了一篇文章記錄了我用Rust重寫一個Linux核心子產品的一些重點體驗,沒想到引起不少人關注。由于第一次寫文章,一些背景沒有詳細介紹,隻專注于寫我想的東西了,導緻一些不了解Rust的人表示有些擔心,是以,我在這裡我補充介紹一點兒背景。

TOC:

  1. 沒有找到原軟體BUG的根因,就貿然用Rust重寫,合适嗎?
  2. 用Rust重寫的代價大嗎?相比用C如何?
  3. Rust這種【進階語言】會不會運作性能差,附加開銷大?

沒有找到原軟體BUG的根因,就貿然用Rust重寫,合适嗎?

由于我們的産品絕大部分采用C語言開發,是以,我們一直被C寫出的記憶體安全問題所困擾。一些影響較大的故障,會導緻各方面的人員高度緊張。我們開發人員要面對就是一旦有緊急情況發生,老道的“專家”們就被召集過來,不管你是在崗還是休息,都要爬到螢幕面前分析解決。基本每年都會有一些這種情況,我就這樣陸續被折磨了十年。

印象最深的一次是我在另一個項目開發攻關的最後一天,通宵到第二天9點,剛上床躺下,就接到上司電話,說我們一個重要客戶的裝置緊急故障,“專家組”其他人已經分析了一陣了,我不得不強行打起精神回到辦公室。經過大家将近一天的努力,初步找到了bug原因,就是我本次重寫的這個核心子產品中的某處記憶體安全問題導緻,與此同時我們另一路人馬正在奔赴他鄉故障現場的路上......。這個已經算是衆多安全問題相對較好分析定位的了。

複雜系統中,C語言的記憶體安全問題遠沒有許多人想象的那麼簡單,通過開發人員的素質教育訓練、良好的測試、費盡心思發明的各種調試方法的确能解決的大部分的安全問題。但是始終會剩下那一小部分,如夢魇般伴随我們的每一天。每當到達一個新的戰場,她們少則十天半月、多則幾個月冒出頭來給我們沉重一擊,她們當中有些很聰明,從不光顧我們的測試環境和實驗環境。

難查的故障這次再一次光臨,我和另一個同僚分析了幾天,沒有準确定位原因,看到代碼裡一些記憶體問題,重構起來改動也非常大,怕改出其它問題。根據故障現象,我們隻能得出一個結論: 這是記憶體安全問題導緻的。而事情又緊急,以至于我們不得不啟用了plan B為客戶解決了問題,公司付出了代價、對于開發人員來講是比較丢臉的事。

問題現場得到緩解之後,按理說我們有足夠的時間來分析解決這個故障,但是,我們真的要這樣年複一年的繼續下去嗎?即使我們再花一兩周時間解決了這個疑難,夢魇仍然不會結束,或許下個月、明年下一個再次光顧。我想,我們需要解決掉這個"根本原因" ---- C。

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

我關注Rust不是一兩天了,2015年偶然發現Rust這個語言,當時沒太在意。我從2016年開始考慮能不能用Rust解決我們的一些問題,也陸續的用Rust寫過一些非産品的小工具。這幾年下來,感覺到了Rust社群的活躍友好、配套工具的便利、語言特性的豐富,最關鍵的是它安全而不妥協性能。安全和性能是我最關注的,編譯器保證我們不能在safe rust中寫出有記憶體問題的代碼,少數地方難免需要用到unsafe塊,盡量把這少量的unsafe邏輯設計得簡單些,安全審計起來也就容易。也算對Rust能否勝任工作有了比較充分的了解。

而Rust相比C唯一的缺點是上手門檻相對較高,這一點同時也是優點。C語言新人往往對語言、記憶體管理、程式整體工作原理知之甚少就能開始在産品上打碼,往往需要留下許多血的教訓才能換來一身的本領。看似練就了桐皮鐵骨金剛不壞之身之後卻仍然頂不住菜刀一擊。而Rust一開始編譯器就會逼迫我們了解這些東西,并且持續為記憶體安全把關,無論新手還是老手隻要不碰unsafe,他就寫不出記憶體問題來(如果有,那一定是其它unsafe的鍋或編譯器bug)。那種更好呢?

那這次這個核心子產品代碼量合适、相對其它元件獨立,是一個我們在生産環境嘗試Rust很好的一個契機,我們希望Rust能切實發揮作用解決問題。

用Rust重寫的代價大嗎?相比用C如何?

此次重寫這個子產品,其實很大一部分精力是花在了解原版的業務邏輯細節和開荒帶來的填坑工作上。開荒的填坑工作是一次性的,後面在這沃土上耕耘将會比較順利。真正的開發工作整個感受是會比用C寫工作量要低,包括造輪子部分。

可能造輪子給人的感覺是比較耗費精力,是不必要的開銷,但實際上這部分工作了很小,我造的那些輪子大部分在使用者态都有對應功能成熟crate,大部分拿過來稍微改幾刀就能用了。而造這些輪子一方面為了安全封裝,另一方面有些輪子單純是為了給後面的業務開發提供便利,有了輪子業務開發會好寫很多。

相反,如果用C,有些個輪子是不會去造的。因為很多情況如果像Rust那樣去造輪子,要麼通用性不好(沒有泛型),要麼根本沒有提供多少友善,缺少輪子該有的意義。是以,用C,業務代碼裡面常常會不經意的忍受一些不友善的寫法。

雖然核心态不能使用标準庫,但即便是no_std領域,我們有core、alloc,以及仍然不少的crate生态,這些現存的輪子使用起來都非常友善。而C呢,不到迫不得已,C程式員是不會去使用第三方庫的,因為C的依賴管理太麻煩了,一些通用功能基本上甯願自己代碼裡面多寫幾個函數也不願去引入一個開源庫。是以,我們看到的C項目往往都是無依賴或屈指可數的幾個依賴。而Rust這種情況就是Cargo.toml裡面加一行字的事兒,友善了許多。

這就導緻用C會比用Rust成本更高一些。

Rust這種【進階語言】會不會運作性能差,附加開銷大?

由于文章裡面有"linux核心"關鍵字,看文章的很多朋友可能沒有了解過Rust,擔心用Rust這樣的進階語言會有附加的運作是開銷,降低性能。

Rust的亮點之一是"零開銷抽象",提供進階抽象能力的同時,不在性能上做妥協。讓我們簡單寫個例子分析一下。

首先,我們用C寫個函數,然後在godbolt.org上看看它的彙編代碼:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

然後,我們用Rust寫一個一樣的:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

可以看到生成的代碼一模一樣。

然後,我們開始添加一些Rust的抽象,看看是不是會影響。首先,用标準庫的sum函數來代替循環:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

彙編代碼仍然一樣,沒有影響。

我們再加一些自己的抽象,引入結構體來表示中間計算過程:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

這裡引入了結構體來中間表示,以及多調用了new,map,fold等函數,但是并沒有增加任何開銷,結果依然不變。

再來看看引入trait:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

可以看到引入trait抽象後仍然絲毫不影響編譯結果。這就是Rust所謂的"零開銷抽象"。

有時候有人會寫一些微小的benchmark來比較C和Rust的性能,比如通過一些高密集的循環計算一些東西來跑分。但是做這種微bench需要高度注意控制變量,不然很容易被誤導。

以下面這邊bench為例

怎麼看Fuchsia官網程式設計語言政策?Go沒有通過,Rust不予提供,建議使用Dart、C/C++?www.zhihu.com

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

答主分别用C和Rust實作了八皇後問題的算法,看誰跑得快。運作結果是Rust比C慢10%-20%, 并給出原因是Rust每次從數組取值都進行邊界檢查, 導緻了整體性能下降。但是我看到評論區有人跑出了相反的結果Rust比C快,為什麼?

我克隆該倉庫直接運作結果是這樣:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

可以看出,在我電腦上非遞歸版C更快, 遞歸版Rust更快,我們繼續探索。

我将queen.rs的被測函數queen稍作修改,插入一些NOP彙編指令:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

運作便得到這樣的結果:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

僅僅是插入了一些NOP指令就提高了程式的性能,神奇吧!

再來改一改queue.c試試,我們隻改它的main函數。原始代碼是這樣:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

給它複制幾份, 變成這樣:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

其它什麼都不動,原則上應該要列印5次幾乎相同的結果。但實際結果是這樣:

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

可以看到那五次成績差異巨大,什麼原因?

我原本以為是和代碼的cache line對齊有關系(不知道cache相關知識的話可以看這篇文章,講得很清楚),因為手動插入NOP或複制queue.c的測試代碼均會改變相關代碼在記憶體中的位置,進而影響執行過程中指令cache miss的次數。但進一步研究發現和CPU cache沒啥關系,用linux perf指令可以給程式統計性能,我用它打出加和不加NOP的兩個程式的差別,發現cache miss都不高,不足以影響測評。

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

上邊加了NOP, 下邊原版

兩個程式運作的指令數差别不大,但加了NOP的版本平均2.2指令/周期,原版隻有2.05,可以看出加了NOP版本許多指令的周期被縮短了。用perf annotate标記出來發現兩者的确有些指令的開銷不一樣。

【Kevin三連彈之二】Rust适合用來寫linux核心子產品嗎?

perf 得到的逐指令cycle開銷百分比,左邊原版,右邊加了NOP

為什麼會這樣?目前對我來說仍然是個謎,麻煩懂行的朋友指點一二哈。(進一步分析見續集)

分析到這裡,至少可以得出一個結論:這種微bench會受到編譯環境諸多因素影響,進而導緻編譯出的機器指令在記憶體中的位置有差異,這樣得出的測評結果完全是這種噪聲造成的差異,不足以說明兩個語言誰快誰慢。該bench中Rust版本的越界檢查的确會産生影響,但理論上影響是微不足道的,不足以造成明顯差異。不要輕易相信類似微測試,容易被誤導。

小結

繼續閱讀