高效Rust代码技巧
How to write fast Rust code
翻译自:
之前该博主尝试将 Lisp 代码移植到 Rust ,结果发现执行效率实在不敢恭维,经过多次优化,性能得到了很大的提升。
让我们从原博主的错误出发,避坑,出发~
误用运算符
let mut n: BigUint = 1u8.into();
let ten: BigUint = 10u8.into();
// 这将分配一个新的 BigUint
n = n * &ten;
// 这不会分配新的实例
n = n * ten;
// 这也不会
n *= ten;
译注:其实就是需要注意使用借用则将重新分配。
Clone 不必要的数据
在 Rust 中每个数据都只有一个所有者,如果你正在使用一个临时数据结构,那么你应该使用数据的借用,而非拷贝数据
// String 数据的拥有者
let mut vec: Vec<String> = Vec::new();
vec.push("a".to_owned());
vec.push("b".to_owned());
{
// 使用借用数据组成的数据结构
let mut temp_vec: Vec<&str> = Vec::new();
// 将原 String 借用为 &str
temp_vec.push(&vec[0]);
}
{
// 有所有权的数据结构
let mut greedy_vec: Vec<String> = Vec::new();
// 你需要 Clone 数据,否则将会报错
greedy_vec.push(vec[1].to_owned());
}
需要注意的是使用包含引用的数据结构通常需要使用生命周期(lifetime),当可以避免克隆数据的成本(尽情的和生命周期 PvP 吧!编译器会慢慢把你“调教”成生命周期人形计算器的~)。
使用低效的的不可变数据结构
关于这一点可能存在一些争议,当使用不可变数据结构时可能会造成一些额外的 Clone,而不可变的数据本身更有效率。但不可否认的时:总是需要使用可变数据结构的。
特别是,当使用 Rust 的基本数据结构,比如 Vec ,任何操作都将克隆整个结构体,而不是像一些函数式语言那样使用 persistent data structures(持久数据结构)更有效率的运行。
因此需要如下优化:
/// before
for word in found_words {
found_word = true;
let mut partial_solution = words.clone();
partial_solution.push(word);
print_translations(num, digits, i + 1, partial_solution, dict)?;
}
/// after
for word in found_words {
found_word = true;
words.push(word);
print_translations(num, digits, i + 1, words, dict)?;
words.pop();
}
Naive! 打印输出
IO 操作是非常缓慢的,除了调试需要,请避免任何不必要的打印操作(这往往经常被大家忽略)
每一次打印都将获取 stdout 上的锁,然后调用 format! 宏解析格式,然后系统调用实际打印(听着就知道这很慢),同时这些所有的操作都是不带缓冲区的(即使有缓冲也于事无补)。
使用 Criterion.rs 评估性能
当我们需要分辨两种写法,谁会谁慢,你需要 Benchmark。Criterion.rs 是一个 Benchmark 微框架。(Rust 自带的 bench 还未 stable)
具体如何使用请查看官方文档吧。(Rust 的 Debug 与 release 性能差距巨大哦)
使用 SmallVec 避免堆分配
众所周知,Rust 的 Vec
是堆分配的,如果已经知道要使用一个非常小的 Vec
那么使用 SmallVec
就值得考虑了,它提供了和 Vec
类似的接口,同时却不是总是堆分配的,仅在需要“溢出”到堆上时,才是堆分配的。
原博主尝试使用它来代替 Vec
保存单词,但是使用 Criterion 进行 Benchmark 测试却并没取得预期的结果,实际性能都出现了不同程度的下降(最好的情况是时间上升了 15%),使用宏基准测试也证实了这一点,最好的情况下,也是二者没有区别
因此这一点需要进一步测试确定。
使用更快的 BigInt 实现
因为不是很常用,这一节我就省略了(懒),总而言之就是,有用,但是不大,我宁愿使用
Vec<u8>
。
使用 bytes 迭代 str 而不是 chars
有人在 Reddit 上建议原博主使用 bytes 而不是 chars 迭代字符传,原本原博主对此表示不信,他认为 chars 应该是零成本的,但是他还是决定一试。
虽然在原博主的代码中,这样的改变仅仅提升了 3% 左右的性能,但是基准测试中,使用 bytes 迭代比使用 chars 迭代几乎快了整整一倍!
所以当你确定你输入的字符串仅包含 ASCII 时,使用 bytes 迭代它吧!
使用 Vec<u8>
代替 BigUint
这样做,在原博主的代码中带来了 7% 的提升,而另一位澳大利亚老哥在他的基础上移除了递归,这带来了 30% 的提升!
所以除非你确定你一定需要,不要使用 BigUint
。
使用压缩字节实现更高效的存储
大多数情况下 Vec
都是极高性能的,但是它却是是堆分配的,相比于执行 CPU 指令的时间相比,还是显得比较昂贵的。
因此,我们需要一个更优化的数据结构来打包传递信息。
涉及原博主的具体算法我就不搬运了,感兴趣可以查看原帖。
使用更快的哈希函数
Rust 的标准库 Hash 使用 DoS-resistant 实现(安全性更好,但是性能并不是最优),当性能优先级很高的情况下,可以使用一些其他 hasher (比如 ahash )作为替代。
原博主的算法在替换后,获得了 20% 到 40% 的性能提升。
Bloom Filter 等概率结构
暂时没看懂 T-T,也是优化数据结构啥的
Todo