From ef62b69a58817c07b0e038bda69a7bee55e707f9 Mon Sep 17 00:00:00 2001 From: sunface Date: Mon, 14 Mar 2022 10:01:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AB=A0=E8=8A=82:=20Rust=20?= =?UTF-8?q?=E9=99=B7=E9=98=B1=20-=20UTF-8=20=E5=BC=95=E5=8F=91=E7=9A=84?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E9=9A=90=E6=82=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SUMMARY.md | 2 +- src/pitfalls/utf8-performance.md | 36 ++++++++++++++++++++++++++++++++ 内容变更记录.md | 4 ++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/pitfalls/utf8-performance.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 9b54831d..e0ffe97a 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -185,7 +185,6 @@ - [类型未限制(todo)](fight-with-compiler/unconstrained.md) - [Rust 常见陷阱](pitfalls/index.md) - - [for 循环中使用外部数组](pitfalls/use-vec-in-for.md) - [线程类型导致的栈溢出](pitfalls/stack-overflow.md) - [算术溢出导致的 panic](pitfalls/arithmetic-overflow.md) @@ -196,6 +195,7 @@ - [奇怪的序列 x..y](pitfalls/weird-ranges.md) - [无处不在的迭代器](pitfalls/iterator-everywhere.md) - [线程间传递消息导致主线程无法结束](pitfalls/main-with-channel-blocked.md) + - [警惕 UTF-8 引发的性能隐患](pitfalls/utf8-performance.md) - [Rust 最佳实践 doing](practice/intro.md) - [日常开发三方库精选](practice/third-party-libs.md) diff --git a/src/pitfalls/utf8-performance.md b/src/pitfalls/utf8-performance.md new file mode 100644 index 00000000..9efd03a6 --- /dev/null +++ b/src/pitfalls/utf8-performance.md @@ -0,0 +1,36 @@ +# 警惕 UTF-8 引发的性能隐患 +大家应该都知道 Rust 的字符串 `&str`、`String` 虽然底层是通过 `Vec` 实现的:字符串数据以字节数组的形式存在堆上,但是在使用时,它们都是 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 的相关操作时,就算不提前优化,也要做到心里有数,这样才能在问题发生时,进退自如。 \ No newline at end of file diff --git a/内容变更记录.md b/内容变更记录.md index 25391e17..7e6c9463 100644 --- a/内容变更记录.md +++ b/内容变更记录.md @@ -1,6 +1,10 @@ # ChangeLog 记录一些值得注意的变更。 +## 2022-03-14 + +- 新增章节: [Rust 陷阱 - UTF-8 引发的性能隐患](https://course.rs/pitfalls/utf8-performance.html) + ## 2022-03-13 - 新增章节: [还 OK 的单向链表 - IterMut](https://course.rs/too-many-lists/ok-stack/itermut.html)