Rust 性能對比:循環(huán) VS 迭代器

2023-03-22 15:12 更新
ch13-04-performance.md
commit 009fffa4580ffb175f1b8470b5b12e4a63d670e4

為了決定使用哪個實現(xiàn),我們需要知道哪個版本的 search 函數(shù)更快一些:是直接使用 for 循環(huán)的版本還是使用迭代器的版本。

我們運行了一個性能測試,通過將阿瑟·柯南·道爾的“福爾摩斯探案集”的全部內(nèi)容加載進 String 并尋找其中的單詞 “the”。如下是 for 循環(huán)版本和迭代器版本的 search 函數(shù)的性能測試結(jié)果:

test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench:  19,234,900 ns/iter (+/- 657,200)

結(jié)果迭代器版本還要稍微快一點!這里我們將不會查看性能測試的代碼,我們的目的并不是為了證明他們是完全等同的,而是得出一個怎樣比較這兩種實現(xiàn)方式性能的基本思路。

對于一個更全面的性能測試,將會檢查不同長度的文本、不同的搜索單詞、不同長度的單詞和所有其他的可變情況。這里所要表達的是:迭代器,作為一個高級的抽象,被編譯成了與手寫的底層代碼大體一致性能代碼。迭代器是 Rust 的 零成本抽象zero-cost abstractions)之一,它意味著抽象并不會引入運行時開銷,它與本賈尼·斯特勞斯特盧普(C++ 的設(shè)計和實現(xiàn)者)在 “Foundations of C++”(2012) 中所定義的 零開銷zero-overhead)如出一轍:

In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
  • Bjarne Stroustrup "Foundations of C++"
從整體來說,C++ 的實現(xiàn)遵循了零開銷原則:你不需要的,無需為他們買單。更有甚者的是:你需要的時候,也不可能找到其他更好的代碼了。
  • 本賈尼·斯特勞斯特盧普 "Foundations of C++"

作為另一個例子,這里有一些取自于音頻解碼器的代碼。解碼算法使用線性預(yù)測數(shù)學(xué)運算(linear prediction mathematical operation)來根據(jù)之前樣本的線性函數(shù)預(yù)測將來的值。這些代碼使用迭代器鏈來對作用域中的三個變量進行了某種數(shù)學(xué)計算:一個叫 buffer 的數(shù)據(jù) slice、一個有 12 個元素的數(shù)組 coefficients、和一個代表位移位數(shù)的 qlp_shift。例子中聲明了這些變量但并沒有提供任何值;雖然這些代碼在其上下文之外沒有什么意義,不過仍是一個簡明的現(xiàn)實中的例子,來展示 Rust 如何將高級概念轉(zhuǎn)換為底層代碼:

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

為了計算 prediction 的值,這些代碼遍歷了 coefficients 中的 12 個值,使用 zip 方法將系數(shù)與 buffer 的前 12 個值組合在一起。接著將每一對值相乘,再將所有結(jié)果相加,然后將總和右移 qlp_shift 位。

像音頻解碼器這樣的程序通常最看重計算的性能。這里,我們創(chuàng)建了一個迭代器,使用了兩個適配器,接著消費了其值。Rust 代碼將會被編譯為什么樣的匯編代碼呢?好吧,在編寫本書的這個時候,它被編譯成與手寫的相同的匯編代碼。遍歷 coefficients 的值完全用不到循環(huán):Rust 知道這里會迭代 12 次,所以它“展開”(unroll)了循環(huán)。展開是一種移除循環(huán)控制代碼的開銷并替換為每個迭代中的重復(fù)代碼的優(yōu)化。

所有的系數(shù)都被儲存在了寄存器中,這意味著訪問他們非常快。這里也沒有運行時數(shù)組訪問邊界檢查。所有這些 Rust 能夠提供的優(yōu)化使得結(jié)果代碼極為高效?,F(xiàn)在知道這些了,請放心大膽的使用迭代器和閉包吧!他們使得代碼看起來更高級,但并不為此引入運行時性能損失。

總結(jié)

閉包和迭代器是 Rust 受函數(shù)式編程語言觀念所啟發(fā)的功能。他們對 Rust 以底層的性能來明確的表達高級概念的能力有很大貢獻。閉包和迭代器的實現(xiàn)達到了不影響運行時性能的程度。這正是 Rust 竭力提供零成本抽象的目標的一部分。

現(xiàn)在我們改進了我們 I/O 項目的(代碼)表現(xiàn)力,讓我們看一看更多 ?cargo? 的功能,他們將幫助我們準備好將項目分享給世界。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號