43mmps
/

【譯】最佳化:讓 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 操作(FileTcpStream,諸如此類)外加上一個 BufReaderBufWriter。這可以輕易的對 I/O 操作增加一層緩衝,將多次寫入或讀取合併成一次,降低 syscall 的使用頻率,增加整體效能。

注意!!:使用 BufWriter時永遠記得在物件釋出前加上flushsync_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 - 更好的 MutexRwLock 實作,而且不會污染你的程式碼(不需要額外的 match/unwrap)。
  • crossbeam-channelflume - 比 std::sync::mspc 更好的 Sender/Receiver 實作。由於程式碼保證 100% Safe,我個人偏好 flume
    譯註:我就喜歡 crossbeam,不服來戰。
  • dashmap - 比 Arc<RwLock<HashMap<K, V>>> 的做法,允許並行使用且有分片最佳化,效率極高且容易使用及轉換。
  • ryulexical - 高效能的數字字串轉換工具,快速將 ”1.2345” 轉換成 1.2345_f32,反之亦同。

在記憶體中配置通往地獄的指標

許多 Rust 開發者在理解缺點前,就把 StringVec 當水喝。這些是所謂的「動態配置」型別。而當我們提到最佳化時,動態配置不會是你的好朋友。

  • 需要在不同格式間序列化(serialized)或反序列化(deserialized)的狀況下,盡量使用 Cow<str> 。這可以允許你在程式碼中借用(borrow)字串,以及需要時轉換成所有(owned)變數。
  • 考慮 tinyvecsmolstr 。這兩個套件可以簡單的讓你的程式碼擁有堆疊最佳化(stack-optimized)資料結構。
  • 需要 clone() 的型別通常都會配置記憶體!盡量使用有 Copy trait 的型別。

另外, jemallocatormimalloc 等記憶體配置器可能可以壓榨出更多效能。

進階魔法擴充

現代處理器有一卡車的擴充指令集,如 AVXSSE。即便在非 x86 的處理器上也有類似的功能,例如 ARM 架構的 NEON ,以及 RISC-V 的 P 與 V 擴充指令集

Rust 允許你直接操作這些指令集介面,而且也有許多高階介面如 packed_simdgeneric-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

成功了!編譯器生成出了 VBROADCASTSSVMOVSS 等 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 所有。

標籤

技術, 翻譯, Rust

延伸閱讀

【譯】學寫程式,就像在下一盤很大的棋

學習程式可以是很殘酷的。你不知道學習的方向是否正確,而且前方總是有很多等著你學。我們大多數人沒有數年的時間用來鞏固程式基礎。

【譯】如何踏出創新的第一步

害怕不完美,是阻止人們創造傑作的最大理由之一。而這種恐懼並不是沒有道理的。許多曠世巨作在初期都會經歷一個階段,一個連創作者看起來都不怎麼起眼的階段。每個創作者都必須面對,並熬過這個階段,才能造就之後的豐功偉業。然而,有很多人並沒有辦到。多數人甚至連「不起眼」的階段都碰不到。他們太害怕了,以至於無法開始。

【譯】如何記住所學的知識

在這篇文章中,我會敘述我的學習流程,你也可以嘗試看看。這套流程適用在任何主題,從程式設計到經濟學都可以。如果你遇到了任何不適用的情境,請讓我知道。

利用 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 的準參賽作品。

catLee

leemiyinghao@gmx.com

初音不是軟體。