比较性能:循环与迭代器
要确定是使用循环还是迭代器,您需要知道哪种实现更快:带有显式
for
循环或带有迭代器的 version 来执行。
我们通过加载 The Adventures of 的全部内容来运行基准测试
夏洛克·福尔摩斯 (Sherlock Holmes) 被阿瑟·柯南·道尔爵士 (Sir Arthur Conan Doyle) 放入一个字符串
中,并在内容中查找单词 the。以下是使用 for
循环的搜索
版本和使用迭代器的版本的基准测试结果:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
迭代器版本稍微快一些!我们不会在这里解释基准测试代码,因为重点不是证明这两个版本是等效的,而是要大致了解这两个实现在性能方面的比较。
为了获得更全面的基准测试,您应该检查使用各种大小的各种文本作为内容
,使用不同的单词和不同长度的单词作为查询
,以及各种其他变体。关键是:迭代器虽然是高级抽象,但被编译为与您自己编写较低级别代码的代码大致相同的代码。迭代器是 Rust 的零成本抽象之一,我们的意思是使用抽象
不会产生额外的运行时开销。这类似于 Bjarne 的方式
C++ 的原始设计者和实现者 Stroustrup 定义了
“Foundations of C++” (2012) 中的零开销:
通常,C++ 实现遵循零开销原则:不使用的,无需付费。此外:你使用的东西,你再好不过了。
作为另一个示例,以下代码取自音频解码器。这
解码算法使用线性预测数学运算来
根据先前样本的线性函数估计未来值。这
代码使用迭代器链对 scope 中的三个变量进行一些数学运算:一个
buffer
数据切片、12 个系数
的数组以及以 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 个值配对。然后,对于每对,我们将值相乘,对所有结果求和,并将 sum qlp_shift
位中的位向右移动。
音频解码器等应用程序中的计算通常优先考虑性能
最高。在这里,我们将使用两个适配器创建一个迭代器,然后
消耗价值。这个 Rust 代码会编译成什么汇编代码?井
在撰写本文时,它将编译为您将手动编写的同一程序集。
根本没有对应于
coefficients
:Rust 知道有 12 次迭代,所以它 “展开” 了循环。展开是一种优化,它消除了循环控制代码的开销,而是为循环的每次迭代生成重复代码。
所有系数都存储在 registers 中,这意味着访问值非常快。运行时对数组访问没有边界检查。Rust 能够应用的所有这些优化使生成的代码非常高效。现在你知道了这一点,你可以放心地使用迭代器和闭包了!它们使代码看起来更高级别,但不会因此而造成运行时性能损失。
总结
闭包和迭代器是受函数式编程语言思想启发的 Rust 功能。它们有助于 Rust 以低级性能清晰地表达高级思想。闭包和迭代器的实现不会影响运行时性能。这是 Rust 努力提供零成本抽象的目标的一部分。
现在我们已经改进了 I/O 项目的表达能力,让我们看看 cargo
的更多功能,这些功能将帮助我们与世界共享项目。