連續 3 年最受歡迎:Rust,香!

阿里技術發表於2020-07-22

連續 3 年最受歡迎:Rust,香!

阿里妹導讀:我們在選擇一種開發語言時會綜合考量各方面的特性,根據實際的需求適當取捨。魚和熊掌往往不可兼得,要想開發效率高,必然要犧牲效能和資源消耗,反之亦然。但是Rust卻出其不意,令人眼前一亮!本文將從效能、記憶體安全、開發效率、跨平臺性及生態等五個方面,對Rust這一程式語言進行一些科普性質的分享。


一  效能對比

不同的語言使用不同的記憶體管理方式,一些語言使用垃圾回收機制在執行時尋找不再被使用的記憶體並釋放,典型的如Java、Golang。在另一些語言中,程式設計師必須親自分配和釋放記憶體,比如C/C++。Rust 則選擇了第三種方式:記憶體被一個所有權系統管理,它擁有一系列的規則使編譯器在編譯時進行檢查,任何所有權系統的功能都不會導致執行時開銷。Rust 速度驚人且記憶體利用率極高,標準Rust效能與標準C++效能不相上下,某些場景下效率甚至高於C++。由於沒有執行時和垃圾回收,它能夠勝任對效能要求特別高的服務。網上已經有了很多關於Rust效能分析對比的文章,不過為了獲得一手的資料,還是自己動手來的更加真實。我選擇了Python,C++,Golang這3種語言來和Rust做效能對比。

效能測試場景設計

同樣的演算法用4種語言分別實現,對比在規定的時間內完成任務的次數。本次測試選擇的演算法是找出10000000以內的所有素數,比較在一分鐘內完成找出所有素數任務的次數。

原始碼連結見[1]。

靜態編譯(或者打包)後生成的二進位制大小對比

rustc++golangpython
249KB129KB1577KB3880KB


結論:(二進位制大小)python > golang > rust > c++


執行速度對比

本場景下比較1分鐘內找出1000000以內所有素數的次數。


rustc++golangpython
第一次85077637830
第二次85978279730
第三次85777739130
第四次82774068431

結論:(執行效率)rust > c++ > golang > python


重點來了,在3臺不同的機器上測試四次的結果顯示:Rust效率居然高於C++!!!

記憶體消耗對比(粗略計算)

rustc++golangpython
30M22M124M190M


結論:(記憶體消耗) python > golang > rust > c++

 
CPU消耗對比(粗略計算)

rustc++golangpython
12.5%12.5%2413


結論:(CPU消耗)golang > python > rust  = c++


以上便是我的測試結果,測試程式碼、二進位制和測試結果參考附件bin.zip,第一次測試後看到結果,有些吃驚,rust的效能居然超過了c++,不可思議,於是又在網上搜尋,找到了別人已經完成的rust效能測試,網上的結果更讓人吃驚,先看第一篇,原始連結見[2]。

我直接截圖看結論:

連續 3 年最受歡迎:Rust,香!
連續 3 年最受歡迎:Rust,香!
以上為Rust vs Golang。

連續 3 年最受歡迎:Rust,香!
連續 3 年最受歡迎:Rust,香!
以上為Rust vs C++。

結論:以上截圖顯示,Rust在效能和資源消耗上不僅大幅度優於Golang,並且和C++效能不相上下,某些場景下效率甚至優於C++。

以上兩種測試場景只是測試一些簡單的演算法,接下來我們看一下在實際使用中的效能資源佔用對比,依然是在網上找到了一篇測試報告[3],該測試報告用Python、PyPy、Go、Rust四種語言實現了一個web後端,接下來使用wrk分別對四個http伺服器進行壓測,該測試場景比較貼近實際,直接截圖看結論:


連續 3 年最受歡迎:Rust,香!

連續 3 年最受歡迎:Rust,香!


結論(效能):在實際作為後端服務使用的場景下,Rust比Golang依然有明顯效能優勢。

連續 3 年最受歡迎:Rust,香!

結論(資源佔用):在記憶體佔用上Rust的優勢更加明顯,只用了Golang的1/3。

綜合以上3個測試,Rust在執行效率和資源消耗上的優勢十分明顯,和C++同一個級別,遠遠優於Golang !

二  記憶體安全性

Rust 最重要的特點就是可以提供記憶體安全保證,而且沒有額外的效能損失。在傳統的系統級程式語言( C/C++) 的開發過程中,經常出現因各種記憶體錯誤引起的崩潰或bug ,比如空指標、野指標、記憶體洩漏、記憶體越界、段錯誤、資料競爭、迭代器失效等,血淚斑斑,數不勝數;記憶體問題是影響程式穩定性和安全性的重大隱患,並且是影響開發效率的重大因素;根據google和微軟 兩大巨頭的說法,旗下重要產品程式安全問題70%由記憶體問題引發[4], 並且兩個巨頭都用利用Rust語言來解決記憶體安全問題的想法。Rust語言從設計之初就把解決記憶體安全作為一個重要目標,透過一系列手段保證記憶體安全,讓不安全的潛在風險在編譯階段就暴露出來。接下來根據自己粗淺的理解,簡單介紹Rust解決記憶體安全的手段有哪些。

1  所有權規則

1)Rust 中每一個值或者物件都有一個稱之為其 所有者(owner)的變數。

例如:
let obj = String::from("hello");

obj是String物件的所有權變數。

2)值或物件有且只能有一個所有者。

3)當所有者離開作用域,所有者所代表的物件或者值會被立即銷燬。

4)賦值語句、函式呼叫、函式返回等會導致所有權轉移,原有變數會失效。

例如:
fn main() {    let s = String::from("hello");    let s1 = s; //所有權發生了轉移,由s轉移給s1    print!("{}",s); //s無效,不能訪問,此句編譯會報錯}
fn test(s1:String){    print!("{}",s1);}
fn main() {    let s = String::from("hello");    test(s); //傳參,所有權發生了轉移    print!("{}",s); //此處s無效,編譯報錯}

Rust的所有權規則保證了同一時刻永遠只有一個變數持有一個物件的所有權,避免資料競爭。

2  借用規則

可能大家都發現了問題,什麼鬼,為什麼我傳了個引數s給test函式,這引數s後面還不能用了呢?如果我接下來要使用變數s怎麼辦?這時候就要用到Rust的借用特性。在Rust中,你擁有一個變數的所有權,如果想讓其它變數或者函式訪問,你可以把它“借”給其它變數或者你所呼叫的函式,供它們訪問。Rust會在編譯時檢查所有借出的值,確保它們的壽命不會超過值本身的壽命。

例如,以下的寫法就沒有問題:

fn test(s1:&String){    print!("{}",s1);}
fn main() {    let s = String::from("hello");    test(&s); //傳參,注意只是傳遞了引用,所有權還歸屬於s    print!("{}",s); //此處s依然有效,可以訪問}

fn main() {    let s = String::from("hello");    let s1 = &s; //s1借用s,所有權還歸屬於s    print!("{}",s); //此處s依然有效,可以訪問    print!("{}",s1); //此處s1和s指向同一個物件}

如果我們嘗試修改借用的變數呢?

fn main() {    let s = String::from("hello");    change(&s);
}
fn change(some_string: &String) {    some_string.push_str(", world");}

借用預設是不可變的,上面的程式碼編譯時會報錯:

error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable --> error.rs:8:5  |7 | fn change(some_string: &String) {  |                        ------- use `&mut String` here to make mutable8 |     some_string.push_str(", world");  |     ^^^^^^^^^^^ cannot borrow as mutable

根據編譯錯誤的提示,透過mut關鍵字將預設借用修改為可變借用就OK,如下程式碼可以編譯透過:
fn main() {    let mut s = String::from("hello");    change(&mut s);
}
fn change(some_string: &mut String) {    some_string.push_str(", world");}

不過可變引用有一個很大的限制:在特定作用域中的特定資料有且只能有一個可變引用,這個限制的好處是 Rust 可以在編譯時就避免資料競爭,這些程式碼會失敗:
let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s;

報錯如下:
error[E0499]: cannot borrow `s` as mutable more than once at a time --> borrow_twice.rs:5:19  |4 |     let r1 = &mut s;  |                   - first mutable borrow occurs here5 |     let r2 = &mut s;  |                   ^ second mutable borrow occurs here6 | }  | - first borrow ends here
 
在存在指標的語言中,容易透過釋放記憶體時保留指向它的指標而錯誤地生成一個 懸垂指標(dangling pointer),所謂懸垂指標是其指向的記憶體可能已經被分配給其它持有者或者已經被釋放。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態:當我們擁有一些資料的引用,編譯器確保資料不會在其引用之前離開作用域。

讓我們嘗試建立一個懸垂引用,Rust 會透過一個編譯時錯誤來避免:
fn main() {    let reference_to_nothing = dangle();
}
fn dangle() -> &String {    let s = String::from("hello");    &s}

這裡是編譯錯誤:
error[E0106]: missing lifetime specifier --> dangle.rs:5:16  |5 | fn dangle() -> &String {  |                ^ expected lifetime parameter  |  = help: this function's return type contains a borrowed value, but there is  no value for it to be borrowed from  = help: consider giving it a 'static lifetime

讓我們簡要的概括一下之前對引用的討論,以下3條規則在編譯時就會檢查,違反任何一條,編譯報錯並給出提示。

1)在任意給定時間,只能 擁有如下中的一個:

  • 一個可變引用。

  • 任意數量的不可變引用。


2)引用必須總是有效的。

3)引用的壽命不會超過值本身的壽命。

3  變數生命週期規則

生命週期檢查的主要目標是避免懸垂引用,考慮以下示例 中的程式,它有一個外部作用域和一個內部作用域,外部作用域宣告瞭一個沒有初值的變數 r,而內部作用域宣告瞭一個初值為 5 的變數x。在內部作用域中,我們嘗試將 r 的值設定為一個 x 的引用。接著在內部作用域結束後,嘗試列印出 r 的值:

error[E0106]: missing lifetime specifier --> dangle.rs:5:16  |5 | fn dangle() -> &String {  |                ^ expected lifetime parameter  |  = help: this function's return type contains a borrowed value, but there is  no value for it to be borrowed from  = help: consider giving it a 'static lifetime

當編譯這段程式碼時會得到一個錯誤:

error: `x` does not live long enough   |6  |         r = &x;   |              - borrow occurs here7  |     }   |     ^ `x` dropped here while still borrowed...10 | }   | - borrowed value needs to live until here

編譯錯誤顯示:變數 x 並沒有 “活的足夠久”,那麼Rust是如何判斷的呢?

編譯器的這一部分叫做 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。如下:r 和 x 的生命週期註解,分別叫做 'a 和 'b:

{    let r;                // -------+-- 'a                          //        |    {                     //        |        let x = 5;        // -+-----+-- 'b        r = &x;           //  |     |    }                     // -+     |                          //        |    println!("r: {}", r); //        |}                         // -------+

我們將 r 的生命週期標記為 'a 並將 x 的生命週期標記為 'b。如你所見,內部的 'b 塊要比外部的生命週期 'a 小得多。在編譯時,Rust 比較這兩個生命週期的大小,並發現 r 擁有生命週期 'a,不過它引用了一個擁有生命週期 'b 的物件。程式被拒絕編譯,因為生命週期 'b 比生命週期 'a 要小:被引用的物件比它的引用者存在的時間更短。

關於借用生命週期檢查,Rust還有一套複雜的生命週期標記規則,使Rust能在編譯時就能發現可能存在的懸垂引用,具體連結見[5]。

4  多執行緒安全保證

記憶體破壞很多情況下是由資料競爭(data race)所引起,它可由這三個行為造成:

  • 兩個或更多指標同時訪問同一資料。

  • 至少有一個這樣的指標被用來寫入資料。

  • 不存在同步資料訪問的機制。


那麼在多執行緒環境下,Rust是如何避免資料競爭的?

先從一個簡單的例子說起,嘗試在另一個執行緒使用主執行緒建立的 vector:

use std::thread;fn main() {    let v = vec![1, 2, 3];    let handle = thread::spawn(|| {        println!("Here's a vector: {:?}", v);    });    handle.join().unwrap();}

閉包使用了 v,所以閉包會捕獲 v 並使其成為閉包環境的一部分。因為 thread::spawn 在一個新執行緒中執行這個閉包,所以可以在新執行緒中訪問 v。然而當編譯這個例子時,會得到如下錯誤:

error[E0373]: closure may outlive the current function, but it borrows `v`,which is owned by the current function --> src/main.rs:6:32  |6 |     let handle = thread::spawn(|| {  |                                ^^ may outlive borrowed value `v`7 |         println!("Here's a vector: {:?}", v);  |                                           - `v` is borrowed here  |help: to force the closure to take ownership of `v` (and any other referencedvariables), use the `move` keyword  |6 |     let handle = thread::spawn(move || {  |                                ^^^^^^^

Rust 會“推斷”如何捕獲 v,因為 println! 只需要 v 的引用,閉包嘗試借用 v。然而這有一個問題:Rust 不知道這個新建執行緒會執行多久,所以無法知曉 v 的引用是否一直有效。所以編譯器提示:
closure may outlive the current function, but it borrows `v` 。

下面展示了一個 v 的引用很有可能不再有效的場景:
use std::thread;fn main() {    let v = vec![1, 2, 3];    let handle = thread::spawn(|| {        println!("Here's a vector: {:?}", v);    });    drop(v); // 強制釋放變數v    handle.join().unwrap();}

為了修復示上面的編譯錯誤,我們可以聽取編譯器的建議:
help: to force the closure to take ownership of `v` (and any other referencedvariables), use the `move` keyword  |6 |     let handle = thread::spawn(move || {

接下來是正確的寫法:

use std::thread;fn main() {    let v = vec![1, 2, 3];    let handle = thread::spawn(move || {  //使用 move 關鍵字強制獲取它使用的值的所有權,接下來就可以正常使用v了        println!("Here's a vector: {:?}", v);    });    handle.join().unwrap();}

從上面簡單例子中可以看出多執行緒間引數傳遞時,編譯器會嚴格檢查引數的生命週期,確保引數的有效性和可能存在的資料競爭。

大家注意到沒有,上面的例子雖然能正確編譯透過,但是有個問題,變數v的所有權已經轉移到子執行緒中,main函式已經無法訪問v,如何讓main再次擁有v呢?如果用C++或者Golang等語言,你可以有很多種選擇,比如全域性變數,指標,引用之類的,但是Rust沒有給你過多的選擇,在Rust中,為了安全性考慮,全域性變數為只讀不允許修改,並且引用不能直接在多執行緒間傳遞。Rust 中一個實現訊息傳遞併發的主要工具是 通道(channel),這種做法時借鑑了Golang的通道,用法類似。

示例:
use std::thread;use std::sync::mpsc;fn main() {    let (tx, rx) = mpsc::channel();    thread::spawn(move || {        let val = String::from("hi");        tx.send(val).unwrap();    });    let received = rx.recv().unwrap();    println!("Got: {}", received);}

上例中,我們可以在main函式中透過channel得到了子執行緒中的物件val。

注意,tx.send(val).unwrap(); 之後,val的所有權已經發生了變化,接下來在子執行緒中不能再對val進行操作,否則會有編譯錯誤,如下程式碼:
use std::thread;use std::sync::mpsc;fn main() {    let (tx, rx) = mpsc::channel();    thread::spawn(move || {        let val = String::from("hi");        tx.send(val).unwrap();        println!("val is {}", val);//在這裡會發生編譯錯誤    });    let received = rx.recv().unwrap();    println!("Got: {}", received);}

這裡嘗試在透過 tx.send 傳送 val 到通道中之後將其列印出來。允許這麼做是一個壞主意:一旦將值傳送到另一個執行緒後,那個執行緒可能會在我們再次使用它之前就將其修改或者丟棄。這會由於不一致或不存在的資料而導致錯誤或意外的結果。對於上面的程式碼,編譯器給出錯誤:
error[E0382]: use of moved value: `val`  --> src/main.rs:10:31   |9  |         tx.send(val).unwrap();   |                 --- value moved here10 |         println!("val is {}", val);   |                               ^^^ value used here after move   |   = note: move occurs because `val` has type `std::string::String`, which doesnot implement the `Copy` trait

我們透過channel能夠實現多執行緒傳送共享資料,但是依然有個問題:通道一旦將一個值或者物件send出去之後,我們將無法再使用這個值;如果面對這樣一個需求:將一個計數器counter傳給10條執行緒,每條執行緒對counter加1,最後在main函式中彙總列印出counter的值,這樣一個簡單的需求如果使用C++或者Golang或者其它非Rust語言實現,非常容易,一個全域性變數,一把鎖,幾行程式碼輕鬆搞定,但是Rust語言可就沒那麼簡單,如果你是一個新手,你可能會經歷如下“艱難歷程”:

首先很自然寫出第一版:
use std::sync::Mutex;use std::thread;fn main() {    let counter = Mutex::new(0);    let mut handles = vec![];    for _ in 0..10 {        let handle = thread::spawn(move || {            let mut num = counter.lock().unwrap();            *num += 1;        });        handles.push(handle);    }    for handle in handles {        handle.join().unwrap();    }    println!("Result: {}", *counter.lock().unwrap());}

多執行緒有了,Mutex鎖也有了,能保證每一次加一都是原子操作,程式碼看起來沒什麼問題,但是編譯器會無情報錯:
error[E0382]: capture of moved value: `counter`  --> src/main.rs:10:27   |9  |         let handle = thread::spawn(move || {   |                                    ------- value moved (into closure) here10 |             let mut num = counter.lock().unwrap();   |                           ^^^^^^^ value captured here after move   |   = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,   which does not implement the `Copy` traiterror[E0382]: use of moved value: `counter`  --> src/main.rs:21:29   |9  |         let handle = thread::spawn(move || {   |                                    ------- value moved (into closure) here...21 |     println!("Result: {}", *counter.lock().unwrap());   |                             ^^^^^^^ value used here after move   |   = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,   which does not implement the `Copy` traiterror: aborting due to 2 previous errors

錯誤資訊表明 counter 值的所有權被move了,但是我們又去引用了,根據所有權規則,所有權轉移之後不允許訪問,但是為什麼會發生?

讓我們簡化程式來進行分析。不同於在 for 迴圈中建立 10 個執行緒,僅僅建立兩個執行緒來觀察發生了什麼。將示例中第一個 for 迴圈替換為如下程式碼:
let handle = thread::spawn(move || {    let mut num = counter.lock().unwrap();    *num += 1;});handles.push(handle);let handle2 = thread::spawn(move || {    let mut num2 = counter.lock().unwrap();    *num2 += 1;});handles.push(handle2);

這裡建立了兩個執行緒並將用於第二個執行緒的變數名改為 handle2 和 num2,編譯會給出如下錯誤:
error[E0382]: capture of moved value: `counter`  --> src/main.rs:16:24   |8  |     let handle = thread::spawn(move || {   |                                ------- value moved (into closure) here...16 |         let mut num2 = counter.lock().unwrap();   |                        ^^^^^^^ value captured here after move   |   = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,   which does not implement the `Copy` traiterror[E0382]: use of moved value: `counter`  --> src/main.rs:26:29   |8  |     let handle = thread::spawn(move || {   |                                ------- value moved (into closure) here...26 |     println!("Result: {}", *counter.lock().unwrap());   |                             ^^^^^^^ value used here after move   |   = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,   which does not implement the `Copy` traiterror: aborting due to 2 previous errors

啊哈!第一個錯誤資訊中說,counter 所有權被移動進了 handle 所代表執行緒的閉包中。因此我們無法在第二個執行緒中再次捕獲 counter , Rust 告訴我們不能將 counter 的所有權移動到多個執行緒中。所以錯誤原因明朗了,因為我們在迴圈中建立了多個執行緒,第一條執行緒獲取了 counter 所有權後,後面的執行緒再也拿不到 counter 的所有權。如何讓多條執行緒同時間接(注意,只能是間接)擁有一個物件的所有權,哦,對了,引用計數!

透過使用智慧指標 Rc<T> 來建立引用計數的值,嘗試使用 Rc<T> 來允許多個執行緒擁有Mutex<T> 於是寫了第二版:
use std::rc::Rc;use std::sync::Mutex;use std::thread;fn main() {    let counter = Rc::new(Mutex::new(0));    let mut handles = vec![];    for _ in 0..10 {        let counter = Rc::clone(&counter);        let handle = thread::spawn(move || {            let mut num = counter.lock().unwrap();            *num += 1;        });        handles.push(handle);    }    for handle in handles {        handle.join().unwrap();    }    println!("Result: {}", *counter.lock().unwrap());}

再一次編譯並…出現了不同的錯誤!編譯器真是教會了我們很多!


















error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36:15:10counter:std::rc::Rc<std::sync::Mutex<i32>>]`  --> src/main.rs:11:22   |11 |         let handle = thread::spawn(move || {   |                      ^^^^^^^^^^^^^ `std::rc::Rc<std::sync::Mutex<i32>>`cannot be sent between threads safely   |   = help: within `[closure@src/main.rs:11:36: 15:10counter:std::rc::Rc<std::sync::Mutex<i32>>]`, the trait `std::marker::Send` isnot implemented for `std::rc::Rc<std::sync::Mutex<i32>>`   = note: required because it appears within the type`[closure@src/main.rs:11:36: 15:10counter:std::rc::Rc<std::sync::Mutex<i32>>]`   = note: required by `std::thread::spawn`

編譯錯誤資訊中有關鍵的一句:
`std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely。

不幸的是,Rc<T> 並不能安全的線上程間共享。當 Rc<T> 管理引用計數時,它必須在每一個clone 呼叫時增加計數,並在每一個克隆被丟棄時減少計數。Rc<T> 並沒有使用任何併發原語,來確保改變計數的操作不會被其他執行緒打斷。在計數出錯時可能會導致詭異的 bug,比如可能會造成記憶體洩漏,或在使用結束之前就丟棄一個值。我們所需要的是一個完全類似 Rc<T>,又以一種執行緒安全的方式改變引用計數的型別。所幸 Arc<T> 正是 這麼一個類似 Rc<T> 並可以安全的用於併發環境的型別。字母 “a” 代表 原子性(atomic),所以這是一個原子引用計數(atomically reference counted)型別。

於是改寫了第三版:
use std::sync::{Mutex, Arc};use std::thread;fn main() {    let counter = Arc::new(Mutex::new(0));    let mut handles = vec![];    for _ in 0..10 {        let counter = Arc::clone(&counter);        let handle = thread::spawn(move || {            let mut num = counter.lock().unwrap();            *num += 1;        });        handles.push(handle);    }    for handle in handles {        handle.join().unwrap();    }    println!("Result: {}", *counter.lock().unwrap());}

這次編譯透過,並且列印出了正確的結果,最終,在嚴厲的編譯器的逐步引導,“諄諄教誨”下,我們總算寫出了正確的程式碼。

Rust編譯器對多執行緒資料共享,多執行緒資料傳遞這種記憶體安全事故多發區進行了極其嚴苛的檢查和限制,確保編譯時就能發現潛在的記憶體安全問題。在多執行緒傳遞資料時,除了透過channel,你沒有第二種選擇;在多執行緒資料共享時,除了Arc+Mutex(如果多執行緒共享的只是int bool這類簡單資料型別,你還可以使用原子操作) ,你同樣沒有別的選擇。雖然 Rust極其缺乏靈活性,但是這同樣是它的有點,因為編譯器一直在逼著你寫出正確的程式碼,極大減少了程式的維護成本。

以上是我對Rust記憶體安全保障手段的一些理解,Rust使用一些乍一看很奇怪的特性,非常清晰的定義了一個安全的邊界,並在上面做以足夠的檢查,保證你的程式碼不會出問題。Rust做到了沒有垃圾回收的記憶體安全,沒有資料競爭的併發安全。同時一個新手Rust程式設計師剛入坑Rust時,大部分的時間都是在解決編譯問題。一個新手C++程式設計師初期可能會寫出很多不安全的程式碼,埋下很多坑,但是新手Rust不會,因為一個新手Rust寫出的不安全程式碼在編譯階段就被攔截了,根本沒有機會埋坑,Rust承諾編譯透過的Rust程式不會存在記憶體安全問題(注意:如果透過unsafe關鍵字強制關閉安全檢查,則依然有可能出現記憶體安全問題)。

三  Rust開發效率問題

關於Rust開發效率問題,沒有一個統一的客觀評價標準,基本靠個人主觀感覺而定。每個人對不同語言掌握的熟練度也是影響開發效率的重要因素。關於開發效率,談一談個人的感受:先說入門,由於Rust一些奇葩的語法的存在(最麻煩的莫過於生命週期標記),導致Rust入門不像Python和Golang等語言那樣輕鬆,但是因為Rust主要是為了替代C/C++這類系統語言而存在,其借鑑了大量C++的語法,如果對C++熟悉,Rust入門不是難事;其次說說開發速度,對於初學者,Rust開發體驗就像在上海開始實行的垃圾分類時上海人民的那種困惑和凌亂,編譯器檢查太嚴格了,大多數時間都是在解決編譯問題,一種在其它語言中理所當然的寫法,在Rust中就是不行,不過好在編譯器的提示非常友好,根據編譯錯誤提示大多數時候能夠找到答案,不過編譯雖然費事,可一旦編譯透過,程式設計師就不需要關心記憶體安全,記憶體洩漏等頭疼問題,只需要關注於業務邏輯,寫了一個多月的Rust,debug次數屈指可數,而且每次debug都是因為業務邏輯,從來沒有因為程式碼記憶體錯誤,崩潰等問題debug;如果對Rust稍微熟練一些,其開發速度絕對不會比Python和Golang慢,因為在編譯階段,Rust就解決了大部分的問題,省去了大量的debug時間。

四  跨平臺性

Rust跨平臺性和Golang一樣,擁有優秀的跨平臺性,支援交叉編譯,一份程式碼可編譯出支援windows、 linux、arm、macos、freebsd等平臺上執行的二進位制,且完全靜態編譯,執行時不依賴任何第三方庫。這個特性對於飽受C++跨平臺編譯折磨的程式設計師來說簡直是福音。Rust對嵌入式環境同樣支援友好,有人用Rust寫了一個簡單的作業系統[6]。

五  生態問題

這一方面應該是Rust最弱的地方,作為一個後起之秀,其生態遠遠不如Python和Golang豐富,不過使用率很高的一些常用庫都能找到;並且Rust連續3年成為Stack Overflow最受歡迎的語言[7],受到的關注度越來越高[8],相信未來Rust的社群一定會越來越豐富。

最後靈魂一問收尾:

沒有垃圾回收的記憶體安全,沒有資料競爭的併發安全、資源消耗低而效能強勁、開發效率高並且跨平臺性優良,這樣的Rust香不香?要不要擁抱一個?

相關連結

[1]https://github.com/famzah/langs-performance
[2]https://benchmarksgameteam.pages.debian.net/benchmarksgame/fastest/rust-gpp.html
[3]https://deavid.wordpress.com/2019/10/12/benchmarking-python-vs-pypy-vs-go-vs-rust/
[4]https://www.chromium.org/Home/chromium-security/memory-safetyhttps://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/
[5]https://www.bookstack.cn/read/trpl-zh-cn/src-ch10-03-lifetime-syntax.md
[6]https://github.com/redox-os/redox
[7]https://stackoverflow.blog/2020/06/05/why-the-developers-who-use-rust-love-it-so-much/
[8]https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f

相關文章