Rust 與 C 的速度比較

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初發表於原作者個人博客,經原作者 Kornel 授權,InfoQ 中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Rust 語言編寫的程序,其運行時速度和內存使用情況應該和用 C 語言編寫的程序相差不大,但是,由於這些語言的整體編程風格不同,所以它們的速度很難一概而論。本文總結了 Rust 和 C 有何相同之處,以及什麼情況 C 更快,什麼情況 Rust 更快。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聲明:本文並非一個客觀的基準,只是揭示了這些語言無可爭辯的事實。這兩種語言理論上能夠實現什麼,以及在實踐中如何使用,存在顯著的差異。這種特別的比較是基於我個人的主觀經驗,包括有交付截止日期、有 Bug,還有懶惰。Rust 語言作爲我的主要編程語言已經超過 4 年了,而之前我使用 C 語言也有 10 年之久。在本文中,我專門將 Rust 與 C 進行比較,因爲與 C++ 相比,將會有更多的“如果”和“但是”,而我並不想深入討論。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡而言之:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 的抽象是把雙刃劍。它們能夠隱藏不良代碼,但是也使得改進算法和利用高度優化的庫更加容易。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我從來沒有擔心過使用 Rust 會陷入性能死衚衕。總是存在一個"},{"type":"text","marks":[{"type":"strong"}],"text":"不安全"},{"type":"text","text":"的逃生艙口,可以進行非常低級的優化(而且通常不需要)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無畏併發(fearless concurrency)確實存在。借用檢查器(borrow checker)偶爾的“笨拙”使並行編程變得實用。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我的總體感覺是,如果可以花費無窮無盡的時間和精力,我的 C 程序將和 Rust 一樣快,甚至比 Rust 還快,因爲在理論上,沒有什麼是 C 做不到而 Rust 可以做到的。但實際上,C 的抽象較少,標準庫很原始,依賴情況也很糟糕,我真的沒有時間每次都重新“發明輪子”。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Rust 和 C 的相似與不同"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"兩者都是“可移植彙編器”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 和 C 都給出了對數據結構佈局、整數大小、堆與堆內存分配、指針間接尋址控制,一般來說,只要編譯器插入一點“魔法”,就可以翻譯成可理解的機器代碼。Rust 甚至承認,字節有 8 位,帶符號的整數可能會溢出!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然 Rust 具有更高級別的結構,比如迭代器、特性(traits)和智能指針,但是這些結構被設計成可以預測的優化直接機器代碼(也就是“零成本抽象”)。Rust 的類型的內存佈局很簡單,例如,可增長的字符串和向量正是 {byte,capacity,length}。Rust 沒有任何像 move 或 copy 構造函數這樣的概念,因此保證對象的傳遞並不比傳遞指針或 memcpy 複雜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"借用檢查只是編譯時的一種靜態分析。在生成代碼之前,它什麼也不做,生命週期信息就被完全剝離了。不存在自動裝箱(autoboxing)之類的聰明做法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 不是“愚蠢”的代碼生成器的一個例子是展開(unwinding)。儘管 Rust 不是用異常來處理正常的錯誤,但是 panic(未處理的致命錯誤)可以有選擇地以 C++ 異常的形式出現。可能會在編譯時禁用(panic = abort),但即便如此,Rust 也不喜歡與 C++ 異常或 longjmp 混在一起。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"老樣子的 LLVM 後端"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 Rust 與 LLVM 集成非常好,因此它支持鏈接時優化(Link-Time Optimization,LTO),包括 ThinLTO,甚至支持跨 C\/C++\/Rust 語言邊界的內聯,還有配置文件引導的優化。雖然 rustc 生成的 LLVM IR 比 clang 冗長得多,但是優化器能夠很好地處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用 GCC 編譯時,我的一些 C 代碼會比 LLVM 更快一些,而且 GCC 沒有 Rust 前端,而 Rust 沒有做到這一點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從理論上講,Rust 允許比 C 更好的優化,因爲它具有更嚴格的不可變性和別名規則,但是實際上這還沒有發生。對於 LLVM,除 C 外的優化工作正在進行,所以 Rust 還沒有充分發揮出它的潛力。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"除少數例外,這兩者都允許手動調優"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 代碼是低級的,而且很容易預測,我可以手動調優它所優化的彙編。Rust 支持 SIMD,能夠很好地控制對內聯、調用約定等。Rust 語言與 C 語言很相似,以至於 C 語言的 profiler 分析器通常可以與 Rust 語言一起使用(例如,我可以在一個 Rust-C-Swift 三明治式程序上使用 Xcode 的工具)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來說,在性能絕對關鍵且需要手工優化到最後一點時,優化 Rust 語言與優化 C 語言之間並無太大差別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些低級的功能,Rust 並沒有合適的替代:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"計算的 goto。goto 的“無聊”用法可以被 loop{break} 等其他 Rust 構造所替代。很多 goto 的用法在 C 語言中是用來清理的,而且由於 RAII\/destructors 的存在,Rust 不需要清理。但是,有一個非標準的 goto *addr 擴展可以用於解釋器。Rust 不能直接執行(你可以寫一個匹配,並希望它能優化),但是另一方面,如果我需要一個解釋器,我將嘗試使用 "},{"type":"link","attrs":{"href":"https:\/\/lib.rs\/crates\/cranelift","title":"xxx","type":null},"content":[{"type":"text","text":"Cranelift JIT"}]},{"type":"text","text":" 來代替。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"alloca 和 C99 可變長度數組。它們甚至在 C 語言中也存在爭議,因此 Rust 語言不會使用它們。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Rust 少量開銷"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,如果 Rust 沒有進行手動調優,則會出現一些低效問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 缺乏針對 "},{"type":"text","marks":[{"type":"strong"}],"text":"usize"},{"type":"text","text":" 進行索引的隱式類型轉換,這促使用戶僅使用該類型,即使在較小的類型足夠時也是如此。和 C 語言形成鮮明對比的是,32 位的 int 是最受歡迎的選擇。通過 "},{"type":"text","marks":[{"type":"strong"}],"text":"usize"},{"type":"text","text":" 索引在 64 位平臺上更容易優化,無需依賴於未定義的行爲,但是額外的位會給寄存器和內存帶來更大的壓力。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"慣用的 Rust 總是將指針和大小傳遞給字符串和切片。在將 C 語言的幾個代碼庫移植到 Rust 之前,我還沒有意識到有多少 C 語言的函數僅僅使用一個指向內存的指針,而沒有任何大小,並且希望得到最佳結果(這些大小可以從上下文中間接地知道,或者僅僅假定它足夠執行該任務)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"並非所有的邊界檢查都得到了優化。"},{"type":"text","marks":[{"type":"strong"}],"text":"用於 arr 中的 item"},{"type":"text","text":" 或者 "},{"type":"text","marks":[{"type":"strong"}],"text":"arr.iter().for_each(...)"},{"type":"text","text":" 都是儘可能高效的,但是如果 "},{"type":"text","marks":[{"type":"strong"}],"text":"i 的形式在 0..len {arr[i]}"},{"type":"text","text":" 中是必需的,那麼性能就取決於 LLVM 優化器能否證明長度匹配。有時候無法進行,約束檢查就會抑制自動向量化(autovectorization)。有各種變通方法,當然,有安全的,也有不安全的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“聰明”地使用內存在 Rust 中不受歡迎。對於 C,任何東西都可以。舉例來說,在 C 語言中,我可能會嘗試將爲某個用途而分配的緩衝區再用於其他用途(這種技術叫做 HEARTBLEED)。對於可變大小的數據(如 PATH_MAX),使用固定大小的緩衝區是很方便的,可以避免(重新)分配不斷增長的緩衝區。慣用的 Rust 仍然對內存分配有很大的控制權,可以做一些基本的事情,如內存池、將多個分配合併爲一個,預分配空間等等,但是總體來說,它會引導用戶使用“無聊”的用法或內存。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果借用檢查規則使事情變得困難,那麼一個簡單的解決辦法就是進行額外的複製或者使用引用計數。久而久之,我學到了很多關於借用檢查器的技巧,並且調整了我的編碼風格,使之更適合於借用檢查器,因此這種情況已經很少發生了。它永遠不會成爲一個大問題,因爲在必要的情況下,總有一個可以回退到“原始”指針。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 的借用檢查器以"},{"type":"link","attrs":{"href":"https:\/\/rust-unofficial.github.io\/too-many-lists\/","title":"xxx","type":null},"content":[{"type":"text","text":"討厭雙向鏈表"}]},{"type":"text","text":"而臭名昭著,但幸運的是,鏈表在目前的硬件上的運行非常緩慢(緩存局部性差,而且沒有向量化)。Rust 的標準庫提供了鏈表,以及更快、更適合於借用檢查器的容器可供選擇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有兩種借用檢查器無法忍受的情況:內存映射文件(來自進程外的神奇變化與引用的不可變性 ^ 排他性語義相沖突)和自引用結構(通過值傳遞結構將內部指針懸空)。這種情況可以通過原始指針解決,就像 C 語言中的每個指針一樣安全,也可以通過心理體操來抽象出這些指針的安全。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Rust 中,單線程程序只是不作爲一個概念存在而已。爲了提高性能,Rust 允許使用單個數據結構而忽視線程安全,但是任何允許在線程之間共享的東西(包括全局變量)必須同步,或者標記爲"},{"type":"text","marks":[{"type":"strong"}],"text":"不安全"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 的字符串支持一些廉價的就地操作,例如 "},{"type":"text","marks":[{"type":"strong"}],"text":"make_ascii_lowercase()"},{"type":"text","text":"(直接與 C 語言中的操作等同),而 "},{"type":"text","marks":[{"type":"strong"}],"text":".to_lowercase()"},{"type":"text","text":" 的複製不需要使用 Unicode-aware 的方式。說到字符串,UTF-8 編碼並不像看上去那麼麻煩,因爲字符串具有 "},{"type":"text","marks":[{"type":"strong"}],"text":".as_bytes()"},{"type":"text","text":" 視圖,所以如果需要的話,可以使用 Unicode-ignorant 的方式來處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"libc 會盡其所能讓 "},{"type":"text","marks":[{"type":"strong"}],"text":"stdout"},{"type":"text","text":" 和 "},{"type":"text","marks":[{"type":"strong"}],"text":"putc"},{"type":"text","text":" 變得相當快。Rust 的 libstd 沒有這麼神奇,因此除非用 "},{"type":"text","marks":[{"type":"strong"}],"text":"BufWriter"},{"type":"text","text":" 進行包裝,否則不會緩衝 I\/O。有些人抱怨說 Rust 比 Python 慢,這是因爲 Rust 花了 99% 的時間逐字節刷新結果,這與我們所說的完全相同。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"可執行文件的大小"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一種操作系統都會內置一些標準的 C 庫,這些 C 庫是 C 可執行文件“免費”得到的約 30MB 的代碼,比如一個小小的“Hello World” C 可執行文件實際上無法輸出任何內容,它只是調用操作系統附帶的 "},{"type":"text","marks":[{"type":"strong"}],"text":"printf"},{"type":"text","text":"。Rust 不能指望操作系統會內置 Rust 的標準庫,因此 Rust 可執行文件捆綁了自己的標準庫(300KB 以上)。幸好,這是可以減少的一次性開銷。在嵌入式開發中,標準庫可以關閉,Rust 將生成“裸”代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 代碼的大小與 C 語言中每個函數的大小相差不多,但存在“泛型膨脹”(generics bloat)的問題。對於每一種類型,都會有泛型函數經過優化的版本,因此有可能同一個函數最終有 8 個版本,"},{"type":"link","attrs":{"href":"https:\/\/lib.rs\/crates\/cargo-bloat","title":"xxx","type":null},"content":[{"type":"text","text":"cargo-bloat"}]},{"type":"text","text":" 工具可以幫助查找它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Rust 中使用依賴關係非常簡單。類似於 JS\/npm ,也有一種製作小型單用途庫的文化,但它們確實是合二爲一。最終,我所有的可執行文件都包含了 Unicode 規範化表、7 個不同的隨機數生成器,以及一個支持 Brotli 的 HTTP\/2 客戶端。在重複數據刪除(deduping)和刪除數據時,cargo-tree 非常有用。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Rust 取得小勝之處"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在討論開銷時,我已經討論了許多,但是 Rust 還存在一些地方,它最終更加高效和快速:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了隱藏實現細節,C 庫經常返回不透明的數據結構指針,並確保結構的每個實例只有一個副本。它會消耗堆分配和指針間接尋址的成本。Rust 內置的隱私、單一所有權規則和編碼慣例允許庫暴露其對象,而不需要間接性,這樣,調用者可以決定將其放入堆(heap)上還是棧(stack)中。可以主動或徹底地優化棧上的對象。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺省情況下,Rust 可以將來自標準庫、依賴項和其他編譯單元的函數內聯。對於 C 語言,我有時不願意拆分文件或使用庫,因爲這會影響內聯,而且需要對頭和符號可見性進行微觀管理。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對結構體字段進行重新排序,減少數據結構的填充(padding)。當用 "},{"type":"text","marks":[{"type":"strong"}],"text":"-Wpadding"},{"type":"text","text":" 編譯 C 語言時,會顯示我有多經常忘記這個細節。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"字符串的大小在它的“胖”指針中進行編碼。這使得長度檢查速度很快,避免了"},{"type":"link","attrs":{"href":"https:\/\/nee.lv\/2021\/02\/28\/How-I-cut-GTA-Online-loading-times-by-70\/","title":"xxx","type":null},"content":[{"type":"text","text":"意外的 O(n²) "}]},{"type":"text","text":"字符串循環,並允許就地生成子串(例如將一個字符串分割成標記),無需通過修改內存或複製來添加 "},{"type":"text","marks":[{"type":"strong"}],"text":"\\0"},{"type":"text","text":" 終止符。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與 C++ 模板類似,Rust 也會爲它們使用的每個類型生成泛型代碼的副本,因此像 sort() 這樣的函數和像哈希表這樣的容器總是針對它們的類型進行優化。對於 C 語言,我必須在修改宏或者處理 "},{"type":"text","marks":[{"type":"strong"}],"text":"void"},{"type":"text","text":"* 和運行時變量大小的效率較低的函數之間做出選擇。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以將 Rust 迭代器組合成鏈,作爲一個單元進行優化。因此,與調用 "},{"type":"text","marks":[{"type":"strong"}],"text":"ibuy(it); use(it); break(it); change(it); mail(upgrade(it));"},{"type":"text","text":" 一系列可能會多次重寫同一緩衝區調用的操作相比,我更願意這樣調用 "},{"type":"text","marks":[{"type":"strong"}],"text":"it.buy().use().break().change().upgrade().mail()"},{"type":"text","text":",編譯成 "},{"type":"text","marks":[{"type":"strong"}],"text":"buy_use_break_change_mail_upgrade()"},{"type":"text","text":",優化後在一次組合傳遞中完成所有這些操作。"},{"type":"text","marks":[{"type":"strong"}],"text":"(0..1000).map(|x| x*2).sum()"},{"type":"text","text":" 編譯爲"},{"type":"text","marks":[{"type":"strong"}],"text":"返回 999000"},{"type":"text","text":"。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,還有"},{"type":"text","marks":[{"type":"strong"}],"text":"讀取"},{"type":"text","text":"和"},{"type":"text","marks":[{"type":"strong"}],"text":"寫入"},{"type":"text","text":"接口,允許函數流式傳輸未緩衝的數據。它們結合得很好,所以我可以把數據寫到一個流中,這個流可以動態地計算數據的 CRC,如果需要,還可以添加 framing\/escaping,對數據進行壓縮,然後將數據寫入網絡,所有這些都是一次性調用。同時,我還可以將這樣的組合流作爲輸出流傳遞給我的 HTML 模板引擎,因此現在每個 HTML 標籤都足夠智能,可以壓縮後發送。底層機制就像普通的 "},{"type":"text","marks":[{"type":"strong"}],"text":"next_stream.write(bytes)"},{"type":"text","text":" 調用的金字塔一樣,所以從技術上講,沒有什麼可以阻止我在 C 語言中做同樣的事情,只是 C 語言中缺乏特性和泛型,這意味着在實際操作中很難做到這一點,而且除了在運行時設置回調之外,其他的效率都不高。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於 C 語言來說,過度使用線性搜索和鏈表是完全合理的,因爲誰會去維護又一個半吊子哈希表的實現呢?由於沒有內置的容器,依賴性非常麻煩,所以我偷工減料去完成任務。我不會去寫一個複雜的 B 樹實現,除非絕對必要。我會用 qsort + bisect,然後收工。在 Rust 中,OTOH 僅需 1 到 2 行代碼就能實現各種容器,其質量非常高。那就意味着我的 Rust 程序每次都可以使用適當的、難以置信的、經過優化的良好數據結構。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如今似乎一切都需要 JSON。Rust 的 "},{"type":"text","marks":[{"type":"strong"}],"text":"serde"},{"type":"text","text":" 是世界上最快的 JSON 解析器之一,它可以直接解析到 Rust 結構中,因此使用解析後的數據也是非常快速和高效的。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Rust 取得大勝之處"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使是在第三方庫中,Rust 也會強制實現所有代碼和數據的線程安全,哪怕那些代碼的作者沒有注意線程安全。一切都遵循一個特定的線程安全保證,或者不允許跨線程使用。當我編寫的代碼不符合線程安全時,編譯器會準確地指出不安全之處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它和 C 語言中的情況完全不同。一般來說,除非庫函數具有明確的文檔說明,否則不能相信它們線程安全。程序員需要確保所有代碼都是正確的,而編譯器對此通常無能爲力。多線程化的 C 代碼有更多的責任和風險,因此假裝多核 CPU 是一種時尚,並且想象用戶有更好的事情可以用剩下的 7 到 15 個核來做,這非常吸引人。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 保證了不受數據爭用和內存不安全的影響(例如,釋放後使用(use-after-free)bug,甚至跨線程)。並非只有一些爭用可以通過啓發式方法或者工具構建在運行時被發現,而是所有的數據爭用都可以被發現。它是救命稻草,因爲數據爭用是並行錯誤中最糟糕的。它們會發生在我用戶的機器上,而不會發生在我的調試器中。也有其他類型的併發錯誤,比如鎖基元使用不當導致更高級別的邏輯爭用條件或死鎖,Rust 無法消除這些錯誤,但它們通常更容易重現和修復。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我不敢用 C 語言在簡單的 for 循環上使用更多的 OpenMP 實用程序。我曾試圖更多地在任務和線程上冒險,但是結果總是令人遺憾。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 已經有了很多庫,如數據並行、線程池、隊列、任務、無鎖數據結構等。有了這類構件的幫助,再加上類型系統強大的安全網,我就可以很輕鬆地並行化 Rust 程序了。有些情況下,用 "},{"type":"text","marks":[{"type":"strong"}],"text":"par_iter()"},{"type":"text","text":" 代替 "},{"type":"text","marks":[{"type":"strong"}],"text":"iter()"},{"type":"text","text":" 是可以的,只要能夠進行編譯,就可以正常工作!這並不總是線性加速( 阿姆達爾定律(Amdahl's law)很殘酷),但往往是相對較少的工作就能加速 2~3 倍。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"延伸:阿姆達爾定律,一個計算機科學界的經驗法則,因 Gene Amdahl 而得名。它代表了處理器並行計算之後效率提升的能力。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在記錄線程安全方面,Rust 和 C 有一個有趣的不同。Rust 有一個詞彙表用於描述線程安全的特定方面,如 Send 和 Sync、guards 和 cell。對於 C 庫,沒有這樣的說法:“可以在一個線程上分配它,在另一個線程上釋放它,但不能同時從兩個線程中使用它”。根據數據類型,Rust 描述了線程安全性,它可以泛化到所有使用它們的函數。對於 C 語言來說,線程安全只涉及單個函數和配置標誌。Rust 的保證通常是在編譯時提供的,至少是無條件的。對於 C 語言,常見的是“僅當 turboblub 選項設置爲 7 時,這纔是線程安全的”。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rust 足夠低級,如果有必要,它可以像 C 一樣進行優化,以實現最高性能。抽象層次越高,內存管理越方便,可用庫越豐富,Rust 程序代碼就越多,做的事情越多,但如果不進行控制,可能導致程序膨脹。然而,Rust 程序的優化也很不錯,有時候比 C 語言更好,C 語言適合在逐個字節逐個指針的級別上編寫最小的代碼,而 Rust 具有強大的功能,能夠有效地將多個函數甚至整個庫組合在一起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,最大的潛力是可以無畏地並行化大多數 Rust 代碼,即使等價的 C 代碼並行化的風險非常高。在這方面,Rust 語言是比 C 語言更爲成熟的語言。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Kornel,程序員,專長圖像壓縮領域。喜歡閒聊。博客寫手。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/kornel.ski\/rust-c-speed"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章