【譯】最佳化:讓 Rust 「RRRRR」
Oct 06, 2020
本文翻譯自 Optimization - Making Rust Code Go Brrrr ,著作權歸 Aspen’s Blog 所有。
Rust 可以執行得非常,非常快。甚至可以在 Benchmarks Game 中與 C/C++ 並駕齊驅。
但即使 Rust LLVM 後端讓一切看起來理所當然,效能並不是憑空出現的。我會示範如何在我的 Rust 專案中改善效能。
Rayon 並不萬能
許多人認為使用 par_iter
在一些簡單運算上可以神奇地讓效能起飛。並不會。這麼做只會讓執行緒同步工作將你生吞活剝。
Rayon 不是只有 par_iter
可以用。例如,par_chunks
會實用得多。它可以將你的運算工作切成平行 chunks,讓每個執行緒同時處理整份資料的一小部分。這樣可以大幅降低同步所需的工作量,尤其是在有大量簡單運算的狀況下特別明顯。當然,某些狀況下使用par_iter
處理繁重運算還是具有優勢的。
iter.par_chunks(4096).for_each(|x| {
for y in x {
y.do_small_thing();
}
});
緩衝(Buffer)很重要!
道理很簡單。I/O 操作牽涉到 syscall,而 syscall 在效能方面惡名昭彰。你會想要盡量避免使用 syscall。
你應該永遠在 I/O 操作(File
、TcpStream
,諸如此類)外加上一個 BufReader
或BufWriter
。這可以輕易的對 I/O 操作增加一層緩衝,將多次寫入或讀取合併成一次,降低 syscall 的使用頻率,增加整體效能。
注意!!:使用 BufWriter
時永遠記得在物件釋出前加上flush
或sync_all
,這一行可以幫助你處理所有可能出現的錯誤。
let fd = File::create("example.bin").expect("Failed to create file!");
let mut writer = BufWriter::new(fd);
std::io::copy(&mut buffer, &mut writer).expect("Failed to copy buffer!");
writer.flush().expect("Failed to write file!");
標準函式庫(std)不一定是最好的
Rust 標準函式庫很棒,真的很棒。但不一定是最佳解。某些套件(crate)可以在幾乎與 std 一模一樣的介面下,提供顯著的效能提升。
parking_lot
- 更好的Mutex
與RwLock
實作,而且不會污染你的程式碼(不需要額外的 match/unwrap)。crossbeam-channel
與flume
- 比std::sync::mspc
更好的Sender/Receiver
實作。由於程式碼保證 100% Safe,我個人偏好flume
。
譯註:我就喜歡 crossbeam,不服來戰。dashmap
- 比Arc<RwLock<HashMap<K, V>>>
的做法,允許並行使用且有分片最佳化,效率極高且容易使用及轉換。ryu
與lexical
- 高效能的數字字串轉換工具,快速將”1.2345”
轉換成1.2345_f32
,反之亦同。
在記憶體中配置通往地獄的指標
許多 Rust 開發者在理解缺點前,就把 String
或 Vec
當水喝。這些是所謂的「動態配置」型別。而當我們提到最佳化時,動態配置不會是你的好朋友。
- 需要在不同格式間序列化(serialized)或反序列化(deserialized)的狀況下,盡量使用
Cow<str>
。這可以允許你在程式碼中借用(borrow)字串,以及需要時轉換成所有(owned)變數。 - 考慮
tinyvec
或smolstr
。這兩個套件可以簡單的讓你的程式碼擁有堆疊最佳化(stack-optimized)資料結構。 - 需要
clone()
的型別通常都會配置記憶體!盡量使用有Copy
trait 的型別。
另外, jemallocator 、 mimalloc 等記憶體配置器可能可以壓榨出更多效能。
進階魔法擴充
現代處理器有一卡車的擴充指令集,如 AVX 與 SSE。即便在非 x86 的處理器上也有類似的功能,例如 ARM 架構的 NEON ,以及 RISC-V 的 P 與 V 擴充指令集。
Rust 允許你直接操作這些指令集介面,而且也有許多高階介面如 packed_simd 或 generic-simd 。但 LLVM 其實是有能力自動幫你編譯成這些指令集的。
要充分利用這項優勢,你需要在編譯用的 RUSTFLAGS 中使用 -C target-cpu=native
或 -C target-features=+avx
。( rustc —-print target-features
可以列出支援的功能,而指令如 lscpu
會幫助你確認自己的處理器有哪些功能 )
- 將任何運算包裝成 4 個或 8 個一組對向量化有利。
- 另外,分支(branch)判斷會嚴重降低向量化的機會。
以下函式會將四個 f32
轉換成 u8
。
#[inline]
pub unsafe fn f32_to_u8(f: f32) -> u8 {
if f > f32::from(u8::MAX) {
u8::MAX
} else {
f32::to_int_unchecked(f)
}
}
#[must_use]
pub fn f32s4_to_u8(f: [f32; 4]) -> (u8, u8, u8, u8) {
let f = &f[..4];
unsafe {
(
f32_to_u8(f[0]),
f32_to_u8(f[1]),
f32_to_u8(f[2]),
f32_to_u8(f[3]),
)
}
}
接下來,我們可以在 Compiler Explorer 中看到這段原始碼產生出的組合語言,記得要加上前面說的編譯參數!
example::f32s4_to_u8:
vmovss xmm0, dword ptr [rip + .LCPI0_0]
vminss xmm1, xmm0, dword ptr [rdi]
vcvttss2si eax, xmm1
vminss xmm0, xmm0, dword ptr [rdi + 4]
vcvttss2si ecx, xmm0
vmovsd xmm0, qword ptr [rdi + 8]
vbroadcastss xmm1, dword ptr [rip + .LCPI0_0]
vcmpleps xmm2, xmm1, xmm0
vblendvps xmm0, xmm0, xmm1, xmm2
vcvttps2dq xmm0, xmm0
vpand xmm0, xmm0, xmmword ptr [rip + .LCPI0_1]
vpsllvd xmm0, xmm0, xmmword ptr [rip + .LCPI0_2]
movzx ecx, cl
shl ecx, 8
movzx eax, al
or eax, ecx
vmovd ecx, xmm0
or ecx, eax
vpextrd eax, xmm0, 1
or eax, ecx
ret
成功了!編譯器生成出了 VBROADCASTSS
和 VMOVSS
等 AVX 指令!
讓編譯器「RRRRR」得更用力
如果想將編譯器設定得更激進,是可以的!例如在 Cargo.toml
裡這樣寫。(注意,這會增加編譯時間!)
[profile.release]
lto = 'thin'
panic = 'abort'
codegen-units = 1
[profile.bench]
lto = 'thin'
codegen-units = 1
設定解釋:
lto = thin
- 開啟 Thin LTO 。也可以嘗試lto=‘fat’
,效果應該會是相同的。
譯註:單純使用lto = true
也是有效的panic = ‘abort’
- Panic 時直接結束程式。這樣一來可以得到比較小、也比較有效率的執行檔,但就無法得知任何 panic 相關的資訊。請參考 Rust 官方手冊 。codegen-units = 1
- 確保你的整個套件(crate)只有一個程式碼生成單元( Code generation unit )。這樣會降低編譯時的平行化,但允許 LLVM 替你優化更多。
本文翻譯自 Optimization - Making Rust Code Go Brrrr ,著作權歸 Aspen’s Blog 所有。
標籤
延伸閱讀
【譯】學寫程式,就像在下一盤很大的棋
學習程式可以是很殘酷的。你不知道學習的方向是否正確,而且前方總是有很多等著你學。我們大多數人沒有數年的時間用來鞏固程式基礎。
【譯】如何踏出創新的第一步
害怕不完美,是阻止人們創造傑作的最大理由之一。而這種恐懼並不是沒有道理的。許多曠世巨作在初期都會經歷一個階段,一個連創作者看起來都不怎麼起眼的階段。每個創作者都必須面對,並熬過這個階段,才能造就之後的豐功偉業。然而,有很多人並沒有辦到。多數人甚至連「不起眼」的階段都碰不到。他們太害怕了,以至於無法開始。
【譯】做為一名工程師,注意力是我提高生產力的最佳資產
翻譯自 zwbetz,版權歸屬於原著。
【譯】如何記住所學的知識
在這篇文章中,我會敘述我的學習流程,你也可以嘗試看看。這套流程適用在任何主題,從程式設計到經濟學都可以。如果你遇到了任何不適用的情境,請讓我知道。
利用 TextAlive App API 與 three.js 製作互動式 PV - Magical Mirai 2020 Programming Contest 入門教學
TextAlive App API 是在 Web 環境中,針對歌曲 PV、MV 的資料擷取工具。本篇教學著重在藉由 TextAlive App API 與 Three.js 製作出符合 Magical Mirai 2020 Programming Contest 的準參賽作品。