Rust 主打安全、高效能兩種性質,企圖打入系統級程式設計(systems programming)競爭圈,我們都知道這圈裡有兩個難以取代、歷史悠久的語言:C、C++。在早年社群時常爭論 C 與 C++ 到底孰優孰劣,時間證明 C++ 不光是 C with class 長年許多優秀的軟體與函式庫都是由 C++ 寫成。井水不犯河水,這個議題也就漸漸在狂熱教徒以外的正常人淡出。
有趣的是,Rust 的發展竟然又再一次引起 Linux之父 Linus Torvalds 對 C++ 的炮火。Linus 對 C++ 的態度社群眾所皆知,起爆點源自於 2007 年有位叫 Dmitry Kakurin 的工程師看了 Git 的原始碼後,做出這樣的評論:
When I first looked at Git source code two things struck me as odd:
1. Pure C as opposed to C++. No idea why. Please don't talk about
portability, it's BS.
2. Brute-force, direct string manipulation. It's both verbose and
error-prone. This makes it hard to follow high-level code logic.
- Dmitry
「用純C而非C++開發?沒道理。別跟我講可移植性,那是鬼扯蛋(bullshit)。」
Linus 看到後作出更猛烈的回擊:"YOU are full of bullshit.",並具體羅列出 C++ 幾項缺陷(詳細的評論翻譯可見文末)。Dmitry Kakurin 也沒停下砲火,道:「恐龍(那些只寫 C 的人)正在滅絕,保持這樣的態度,你很快就會發現自己孑然一身。("As dinosaurs (who code exclusively in C) are becoming extinct, you will soon find yourself alone with attitude like this.")」 迄今,我們都知道 C 並沒有滅絕,反而還是常年排行榜霸主。
而今年又一次引來 Linus 對 C++ 的嘲諷,要從 Android 說起。因為 Google 評估 Rust 後決定在 Android 正式加入 OS 開發語言體系,而目前正被評估成為 Linux Kernel 的一部份。此時便有人評論:「有個更簡單的解法:使用 C++ 而非 Rust」,在iTWire的訪談中 Linus 忍不住又將砲口再一次瞄向C++直言:「C++ 沒解決半點 C 的問題,只把事情弄得更糟("C++ solves _none_ of the C issues, and only makes things worse.")」
系統級開發,我們往往會聯想到高效能、穩定性,而這兩點都與記憶體還有計算機原理脫不了關係。學習語言我最常實驗的題目就是檔案讀取,因為:
- 大部分的程式需要進行檔案存取
- 從檔案存取的介面我們能看出這個語言對記憶體的操作能力、函式庫的設計風格
- 檔案存取需要基本的容錯處理
fn method(file: &std::fs::File) -> u64 {
let mut sum: u64 = 0;
let mut it = file.bytes();
while let Some(x) = it.next() {
sum += x.unwrap() as u64;
}
return sum;
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let filepath = Path::new(&args[1]);
let mut file = match std::fs::File::open(filepath) {
Ok(f) => f,
Err(_) => {
panic!();
}
};
let mut start = std::time::Instant::now();
let ans1 = method(&file);
let mut duration = start.elapsed();
println!("{:?}", duration);
}
這是我們第一個版本,按理來說 Argument Parser 應該要對指令參數作檢查,但這裡我們就比較偷懶地略過這個步驟。
std::fs::File::open(filepath) 並不像 C 或 C++ 直接回傳檔案的 descriptor 或 object(這也是這兩個語言在設計哲學上很大的不同之處),而是一個Result。標準庫的Result只有兩種值,一種是成功取得的實際物件型別──以開啟檔案例子來說是File物件──一種是錯誤值。在沒有對這個Result進行判斷前,我們就無法對實際物件操作。
過去我們使用 C++ 物件時,會有兩種狀況:
- 初始化物件,但物件的底層操作失敗,需要透過物件的介面進行檢查
- 初始化物件,但物件的底層擲出例外,需要進行例外處理
我們總是要透過標註、流程檢查,再將這些不正常運作的物件正確地釋放掉。「不清楚會發生什麼」是令人畏懼的,特別是對於那些不了解實作細節的人而言寫起來總有點不踏實的感覺。Rust 的標準函式庫突顯出一個重要的設計哲學,這個結果要不是成功就是失敗的,如果它是成功的就應該放心去使用它。因此,檢查成為開發必然的一環。
那麼要如何有效地進行檢查呢?Rust 是一個以表示式為主的語言,表示式象徵求值。剛開始學習 Rust 的我們能注意到 match 這個利器,它就是一個 expression,而結果總是一個值。
let mut file = match std::fs::File::open(filepath) {
Ok(f) => f,
Err(_) => {
panic!();
}
}
Ok(f) 與 Err(_) 這類寫在 => 前的稱之為 match arm,也就是一個以上 pattern 所組成的條件,Rust 的 pattern 模式非常豐富,這裡只先介紹最基本的。Ok(f)、Err(_) 其實都是一種 pattern,關鍵的是括號內被 bind 的 value。 f 是我們在 match expression 內用來識別符合 Ok(f) 這種模式的變數的 binding,根據類型 f 有可能被 move 或是 copy。_ 則是 wildcard,熟悉 Python 的朋友應該不陌生,當我們不在乎這個數值甚至不想要承接這個數值的操作成本時,_ 並不會導致copy、move、borrow三種行為。
那麼有趣的問題就來了,我們知道 Rust 是一個強型別也具備型別推導能力的語言,我們可以肯定 file 必須是也只能是 std::fs::File,而 match 這個 expression 會產生一個值,當檔案開啟成功時 Ok(f) => f 會得到 unwrap 過後的實際物件,而檔案開啟失敗時 Err(_) => { panic!(); } 則會......咦?是不是哪裡怪怪的阿? 這也是 Rust 有趣的地方之一,它有個型別叫作 never。never顧名思義,這個計算不會產生任何值,例如:break、return、continue。官方用一個很簡單的例子說明:
fn main() {
fn get_a_number() -> Option<u32> { None }
loop {
let num: u32 = match get_a_number() {
Some(num) => num,
None => break,
};
}
}
同理,panic!這個巨集會終止程式,因此 file 究竟有沒有值,對於程式來說也不重要了。
接著看看檔案讀取的部分,為了測試效能我們將檔案中每個 byte 讀出來作加種。
fn method(file: &std::fs::File) -> u64 {
let mut sum: u64 = 0;
let mut it = file.bytes();
while let Some(x) = it.next() {
sum += x.unwrap() as u64;
}
return sum;
}
雖然我們獲得了一個正確的結果,但仔細測量時間會發現,這個程式慢得不可思議(與C、C++相比)。為何呢?因為在這個讀取流程中,我們並沒有善用 buffer,導致了程式必須頻繁地存取檔案。我們可以試試看第二個方法BufReader:
fn method_2(file: &std::fs::File) -> u64 {
let mut sum: u64 = 0;
let reader = std::io::BufReader::with_capacity(1024, file);
let mut it = reader.bytes();
while let Some(x) = it.next() {
sum += x.unwrap() as u64;
}
return sum;
}
有了BufReader,效能可以說是顯著的提升,然而在開發時我卻遇到了幾個問題。BufReader其實限制不少,最頭疼的一點就是他很難地直接控制位置,考慮到BufReader實作不少類型,在必須兼具泛用的程度下無可厚非。不過從 BufReader 的觀察給了我們一個重要的提示,那就是減少檔案存取次數似乎便能有效地提升效能。真是如此嗎?這次我們不使用BufReader,來自己實現利用 buffer 機制的讀取:
fn method_3(file: &mut std::fs::File) -> u64 {
let mut sum: u64 = 0;
let mut buffer = unsafe { std::mem::MaybeUninit::<[u8; 1024]>::uninit().assume_init() };
while let Ok(l) = file.read(&mut buffer[..]) {
if l == 0 {
break;
}
for i in 0..l {
sum += buffer[i] as u64;
}
}
return sum;
}
同樣使用 1024 bytes,不使用 iterator 介面手動處理 buffer 的作法,甚至能給我更快的速度。透過這個實驗,我們也能發現 rust 並不會神奇地讓你輕鬆寫出高效能的程式,而是與 C、C++ 一樣,我們必須對底層有所了解、也要掌握語言本身,才能正確地寫出兼具效能與安全的程式。
附錄
*YOU* are full of bullshit.
C++ is a horrible language. It's made more horrible by the fact that a lot
of substandard programmers use it, to the point where it's much much
easier to generate total and utter crap with it. Quite frankly, even if
the choice of C were to do *nothing* but keep the C++ programmers out,
that in itself would be a huge reason to use C.
In other words: the choice of C is the only sane choice. I know Miles
Bader jokingly said "to piss you off", but it's actually true. I've come
to the conclusion that any programmer that would prefer the project to be
in C++ over C is likely a programmer that I really *would* prefer to piss
off, so that he doesn't come and screw up any project I'm involved with.
C++ leads to really really bad design choices. You invariably start using
the "nice" library features of the language like STL and Boost and other
total and utter crap, that may "help" you program, but causes:
- infinite amounts of pain when they don't work (and anybody who tells me
that STL and especially Boost are stable and portable is just so full
of BS that it's not even funny)
- inefficient abstracted programming models where two years down the road
you notice that some abstraction wasn't very efficient, but now all
your code depends on all the nice object models around it, and you
cannot fix it without rewriting your app.
In other words, the only way to do good, efficient, and system-level and
portable C++ ends up to limit yourself to all the things that are
basically available in C. And limiting your project to C means that people
don't screw that up, and also means that you get a lot of programmers that
do actually understand low-level issues and don't screw things up with any
idiotic "object model" crap.
So I'm sorry, but for something like git, where efficiency was a primary
objective, the "advantages" of C++ is just a huge mistake. The fact that
we also piss off people who cannot see that is just a big additional
advantage.
If you want a VCS that is written in C++, go play with Monotone. Really.
They use a "real database". They use "nice object-oriented libraries".
They use "nice C++ abstractions". And quite frankly, as a result of all
these design decisions that sound so appealing to some CS people, the end
result is a horrible and unmaintainable mess.
But I'm sure you'd like it more than git.
Linus
你才鬼扯蛋。
C++ 是門糟糕的語言。讓 C++ 更糟糕的是有一群不合格的程式設計師在使用它,因此
更更更容易作出整坨屎。老實講,光是用 C 語言就能趕跑 C++ 工程師這點,就是使用
C 最大的理由。
這樣說吧,C 語言是唯一合理的選擇。我知道 Miles Bader 開玩笑地說:是為了氣死你
們。不過這是真的。我得出的結論就是如果任何想參與這個專案卻更想用 C++ 而非 C 的
程式設計師大概就是我想搞走的人,這樣他才不會來搞砸任何我有參與的專案。
C++ 走入一個非常非常糟糕的設計抉擇。你們總老使用那些自稱"美妙"語言特徵的函式庫
諸如 STL 和 Boost 還有其他垃圾,也許那能夠"幫助"你們開發,但導致:
- 當他們出錯時所帶來永無止盡的痛苦(如果有人告訴我STL尤其是Boost是非常穩定而且
具可移植性,不好笑,根本鬼扯蛋)
- 低效的抽象設計模型搞了你兩年你才發現他們並沒有用,但你的程式都是根據這些所謂
美好的物件模型來設計的,你根本沒有辦法修正只好整個重寫。
換句話說,C++ 唯一能做到良好、有效、可移植性的系統級開發方式,就是限制自己只做那
些 C 就能做到的事。如果你用 C 來開發,意味著別人沒法搞砸你的專案,也意味著那些真
正懂得底層問題的程式設計師不會用什麼狗屁物件模型來搗亂。
所以我很抱歉,但對於 git 這類效能導向的專案來說,所謂 C++ 的"優點"根本大錯特錯
。除此之外最大的優點就是我們能惹惱你們這些看不清實情的傢伙。
如果你想要用 C++ 寫成的版本控制系統,去玩 Monotone 吧。真的,他們用了"真正的資
料庫"。他們使用了"優秀的物件導向函式庫"。他們使用了"優秀的C++抽象化"。但說實在的
,他們的設計決策也許吸引一些資訊科學人士,但最後的結果只會是可怕、難以維護的一團
糟。
不過你肯定喜歡它更勝過 git。
Linus