|
|
|
@ -0,0 +1,36 @@
|
|
|
|
|
# 警惕 UTF-8 引发的性能隐患
|
|
|
|
|
大家应该都知道 Rust 的字符串 `&str`、`String` 虽然底层是通过 `Vec<u8>` 实现的:字符串数据以字节数组的形式存在堆上,但是在使用时,它们都是 UTF-8 编码的,例如:
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let s: &str = "中国人";
|
|
|
|
|
for c in s.chars() {
|
|
|
|
|
println!("{}", c) // 依次输出:中 、 国 、 人
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let c = &s[0..3]; // 1. "中" 在 UTF-8 中占用 3 个字节 2. Rust 不支持字符串索引,因此只能通过切片的方式获取 "中"
|
|
|
|
|
assert_eq!(c, "中");
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
从上述代码,可以很清晰看出,Rust 的字符串确实是 UTF-8 编码的,这就带来一个隐患:可能在某个转角,你就会遇到来自糟糕性能的示爱。
|
|
|
|
|
|
|
|
|
|
## 问题描述 & 解决
|
|
|
|
|
例如如果你尝试写一个词法解析器,里面用到了以下代码 `self.source.chars().nth(self.index).unwrap();` 去获取下一个需要处理的字符,大家可能会以为 `.nth` 的访问应该非常快吧?事实上它确实很快,但是这段代码在循环处理 70000 长度的字符串时,需要消耗 5s 才能完成!
|
|
|
|
|
|
|
|
|
|
这么看来,唯一的问题就在于 `.chars()` 上了。
|
|
|
|
|
|
|
|
|
|
其实原因很简单,简单到我们不需要用代码来说明,只需要文字描述即可传达足够的力量:每一次循环时,`.chars().nth(index)` 都需要对字符串进行一次 UTF-8 解析,这个解析实际上是相当昂贵的,特别是当配合循环时,算法的复杂度就是平方级的。
|
|
|
|
|
|
|
|
|
|
既然找到原因,那解决方法也很简单:只要将 `self.source.chars()` 的迭代器存储起来就行,这样每次 `.nth` 调用都会复用已经解析好的迭代器,而不是重新去解析一次 UTF-8 字符串。
|
|
|
|
|
|
|
|
|
|
当然,我们还可以使用三方库来解决这个问题,例如 [str_indices](https://crates.io/crates/str_indices)。
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
最终的优化结果如下:
|
|
|
|
|
|
|
|
|
|
- 保存迭代器后: 耗时 `5s` -> `4ms`
|
|
|
|
|
- 进一步使用 `u8` 字节数组来替换 `char`,最后使用 `String::from_utf8` 来构建 UTF-8 字符串: 耗时 `4ms` -> `400us`
|
|
|
|
|
|
|
|
|
|
**肉眼可见的巨大提升,12500 倍!**
|
|
|
|
|
|
|
|
|
|
因此我们在热点路径使用字符串做 UTF-8 的相关操作时,就算不提前优化,也要做到心里有数,这样才能在问题发生时,进退自如。
|