性能比较:循环 vs. 迭代器
为了确定是使用循环还是迭代器,你需要知道哪个实现更快:使用显式 for
循环的 search
函数版本,还是使用迭代器的版本。
我们运行了一个基准测试,方法是将阿瑟·柯南·道尔爵士的《福尔摩斯冒险记》的全部内容加载到一个 String
中,并在内容中查找单词 the。以下是使用 for
循环的 search
版本和使用迭代器的版本进行基准测试的结果
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
这两个实现的性能相似!我们不会在此处解释基准测试代码,因为重点不是要证明这两个版本是等效的,而是要大致了解这两个实现如何在性能方面进行比较。
为了进行更全面的基准测试,您应该检查使用各种大小的各种文本作为 contents
,不同的单词和不同长度的单词作为 query
,以及各种其他变体。重点是:迭代器虽然是一种高级抽象,但会被编译成与您自己编写的低级代码大致相同的代码。迭代器是 Rust 的零成本抽象之一,我们的意思是使用抽象不会增加额外的运行时开销。这类似于 Bjarne Stroustrup(C++ 的原始设计者和实现者)在“C++ 基础”(2012 年)中定义零开销的方式
总的来说,C++ 实现遵循零开销原则:不用的东西,就不用为此付费。更进一步:你用的东西,你不可能手动编码得更好。
作为另一个例子,以下代码取自音频解码器。解码算法使用线性预测数学运算来基于先前样本的线性函数估计未来值。此代码使用迭代器链对作用域中的三个变量执行一些数学运算:数据 buffer
切片、12 个 coefficients
的数组以及在 qlp_shift
中移动数据的量。我们在此示例中声明了变量,但没有赋予它们任何值;虽然此代码在其上下文之外没有太多意义,但它仍然是 Rust 如何将高级思想转换为低级代码的简洁、真实的示例。
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
方法将系数值与 buffer
中先前的 12 个值配对。然后,对于每对,我们将值相乘在一起,将所有结果求和,并将总和中的位向右移动 qlp_shift
位。
音频解码器等应用程序中的计算通常将性能放在首位。在这里,我们正在创建一个迭代器,使用两个适配器,然后消耗该值。此 Rust 代码将编译成什么汇编代码?嗯,截至撰写本文时,它被编译成与您手动编写的汇编代码相同的代码。根本没有与迭代 coefficients
中的值相对应的循环:Rust 知道有 12 次迭代,因此它“展开”了循环。展开是一种优化,它消除了循环控制代码的开销,而是为循环的每次迭代生成重复代码。
所有系数都存储在寄存器中,这意味着访问这些值非常快。在运行时对数组访问没有边界检查。Rust 能够应用的所有这些优化使生成的代码非常高效。现在您知道了这一点,您可以放心地使用迭代器和闭包!它们使代码看起来更高级,但不会因此而带来运行时性能损失。
总结
闭包和迭代器是受函数式编程语言思想启发的 Rust 功能。它们有助于 Rust 以低级性能清晰地表达高级思想的能力。闭包和迭代器的实现方式使得运行时性能不受影响。这是 Rust 努力提供零成本抽象的目标的一部分。
既然我们已经提高了 I/O 项目的表达能力,那么让我们看一下 cargo
的更多功能,这些功能将帮助我们与世界分享该项目。