--- name: optimizing-rust-performance description: | 审查、重构或编写 Rust 代码时主动识别性能瓶颈并应用优化模式。触发关键词: "optimize"、"perf"、"性能优化"、"加速"、"hot path"、"热点路径"、 "make it faster"、"reduce allocation"、"zero-copy"、"零拷贝"、 以及任何对 .rs 文件的 review/refactor 请求、或代码中出现 Vec::remove / clone / Vec 在循环中 / String::from / filter 后 collect 等可疑模式时。 allowed-tools: - Read - Edit - Grep - Glob metadata: trigger: Rust 代码性能审查 / 重构 / 热点路径优化 / 减少 heap 分配 source: 基于 Rust 性能优化通用最佳实践 + 真实 baseline 测试提炼 --- # Rust 性能优化(Performance Optimization) 编写或审查 Rust 代码时,**主动**识别性能瓶颈并应用优化模式。核心原则: **按固定优先级排序**,**判断触发条件即应用**,**量化收益**,**用 profiling 验证**。 ## 优化心智模型(必须按此顺序判断) ``` 优化不是"想到什么改什么"。永远按此优先级评估: 1. 算法与复杂度 — O(n)→O(1)、O(n²)→O(n log n)。最大收益,优先看。 2. 内存分配 — 减少 heap 分配,栈优先,能零拷贝就零拷贝。 3. 数据布局 — cache locality、字段顺序、对齐、SoA vs AoS。 4. 并发 — 减少锁竞争,考虑 lock-free / 无锁结构。 低层级优化在高层级问题存在时收益微乎其微。先看复杂度,再看分配。 ``` ## 铁律:判断到触发条件就应用,不要只"提及" > **发现反模式 → 直接改。不要说"可以考虑用 X"然后不改。** > > 如果触发条件命中,你必须(a)在改进后的代码里实际应用该模式, > (b)说明改了什么、为什么快。把"应该用 Cow"挂在嘴边却不写进代码, > 等于没优化。 ## 核心模式(触发条件 → 动作) ### 模式 1:集合删除(顺序无关时) **触发**:代码用 `.remove(index)` 删除 `Vec` 元素,且调用方不关心顺序。 **动作**:改用 `.swap_remove(index)` —— O(1),把末尾元素换到被删位置,无内存搬移。 ```rust // ❌ O(n):删中间元素要把后面所有元素左移 self.items.remove(index); // ✅ O(1):末尾元素换位,不保证顺序 self.items.swap_remove(index); ``` **收益**:O(n) → O(1) 的内存搬移。 ### 模式 2:集合过滤 **触发**:代码用 `for` 循环 + 条件 `push` 到新 `Vec`,或 `filter()` 后 `collect()` 再赋值。 **动作**:用 `.retain()` / `.retain_mut()` 原地 O(n) 过滤,避免第二个 `Vec` 分配和逐元素 `clone`。 ```rust // ❌ 分配第二个 Vec + 每个元素 clone let mut kept = Vec::new(); for item in &self.items { if item.in_stock { kept.push(item.clone()); } } self.items = kept; // ✅ 原地过滤,零额外分配,零 clone self.items.retain(|item| item.in_stock); ``` **收益**:省一次堆分配 + N 次 clone。 ### 模式 3:所有权转移,避免 clone **触发**:从 `&mut T` / `Option` 取值时用了 `.clone()`,或想"取出旧值替换为默认"。 **动作**: - `Option` 取值 → `option.take()`(取出并留 `None`,无 clone) - `T: Default` 取出旧值 → `std::mem::take(dest)`(旧值返回,`dest` 变默认) - 交换值 → `std::mem::replace(dest, src)`(无深拷贝) ```rust // ❌ clone 后原值仍在,字段没被清空(还可能是 bug) fn clear_promo(&mut self) -> Option { self.active_promo.clone() } // ✅ take:取出所有权,字段变 None,无 clone fn clear_promo(&mut self) -> Option { self.active_promo.take() } ``` **收益**:消除一次堆分配(clone 的 `String`/`Vec` 等)。 ### 模式 4:栈优先(短生命周期小集合) **触发**:循环内频繁分配小而短命的 `Vec`/`String`(如"通常 2-3 个元素"的辅助返回值)。 **动作**:用 `SmallVec`/`TinyVec` 把小负载(<4 或 <8 元素)放栈上,溢出才上堆。 ```rust // ❌ 每次 tags_for 都堆分配一个 Vec(通常只有 2-3 个 tag) fn tags_for(&self, id: u32) -> Vec { vec![format!("tag-{id}-a"), format!("tag-{id}-b")] } // ✅ SmallVec:2-3 个元素常驻栈,无堆分配 fn tags_for(&self, id: u32) -> SmallVec<[String; 4]> { smallvec![format!("tag-{id}-a"), format!("tag-{id}-b")] } ``` **收益**:栈分配 vs 堆分配——省掉 malloc/free 和可能的 cache miss。 ### 模式 5:Copy-on-Write 延迟分配 **触发**:字符串/切片处理大多只读,偶尔才需要修改或拥有所有权;返回类型是 `String` 但其实常无需分配。 **动作**:用 `std::borrow::Cow` 封装——只读时零分配借用,真正写时才 clone。 ```rust use std::borrow::Cow; // ❌ 永远堆分配,即使输入已经全是小写无需改动 fn normalize(name: &str) -> String { name.trim().to_lowercase() } // ✅ 只在确实需要改写(trim 砍掉字符 / 含大写)时才分配; // 纯小写无空白的输入零分配,原样借用返回 fn normalize<'a>(name: &'a str) -> Cow<'a, str> { let trimmed = name.trim(); let needs_lower = trimmed.chars().any(|c| c.is_ascii_uppercase()); let needs_trim = trimmed.len() != name.len(); if !needs_lower && !needs_trim { Cow::Borrowed(trimmed) } else { Cow::Owned(trimmed.to_lowercase()) } } ``` **收益**:读路径零分配;分配延迟到真正写时才发生。 ## 热点路径额外模式 热点路径(每秒百万次调用的解析器/词法器/序列化)适用更激进的优化: - **切片代替逐字符 collect**:从原 `&str`/`&[u8]` 用范围切片 `&input[start..pos]` 取词素,而不是把 `char` push 进 `Vec` 再 `collect::()`(省双重分配 + 双重拷贝)。 - **借用切片匹配后再拥有化**:先在借用的 `&str` 上 match 关键字(零分配),仅非关键字才 `.to_string()`。 - **ASCII 谓词代替 Unicode 谓词**:`is_ascii_digit()` / `is_ascii_alphabetic()` 代替 `is_digit(10)` / `is_alphanumeric()`(省 Unicode 表查找)——前提是你确实只需 ASCII。 ## 回应规范(应用优化时必须做到) 1. **先分析瓶颈**:说明当前问题("这会导致 O(n) 内存搬移" / "这触发一次堆分配")。 2. **给出优化代码**:干净、生产可用的 Rust,实际应用模式(不只是提及)。 3. **量化理论收益**:说明*为什么*更快(堆 vs 栈、O(n) vs O(1)、cache locality)。 4. **提示 profiling**:提醒用 `Criterion`(微基准)或 `Flamegraph` 验证,不要盲信理论。 **量化收益示例**(写到改动说明里): | 改动 | 复杂度 / 分配变化 | | -------------------------- | --------------------------- | | `remove` → `swap_remove` | O(n) → O(1) 内存搬移 | | 循环+clone → `retain` | 省 1 次堆分配 + N 次 clone | | `clone` → `take` | 省 1 次堆分配 | | `Vec` → `SmallVec<[T; 4]>` | 堆分配 → 栈分配(小负载时) | | `String` → `Cow` | 读路径:1 次分配 → 0 次 | ## 自检清单(提交 Rust 改动前逐条过) - [ ] 任何 `Vec::remove` 在顺序无关处是否已换 `swap_remove`? - [ ] 过滤操作是否用了 `retain` / `retain_mut` 而非 collect? - [ ] 取值是否用了 `take` / `mem::take` / `mem::replace` 而非 `clone`? - [ ] 循环内小而短命的 `Vec`/`String` 是否考虑过 `SmallVec`? - [ ] 大多只读、偶尔写的字符串/切片是否考虑过 `Cow`? - [ ] 热点路径是否切片取词素而非逐字符 collect? - [ ] 每条改动是否说明了复杂度/分配收益,并提示 profiling 验证? - [ ] 引入新依赖(`smallvec` 等)或改动公开返回类型前,确认收益配得上成本。 ## 注意(应用前必读 —— 防止过度优化) 优化有成本。应用下列模式前先权衡,**别为了用模式而用**: - **先正确再优化**:`#[inline]`、手动 SIMD、`unsafe` 等通常不是首选;先确认算法复杂度和分配已最优。 - **新增依赖有代价**:`SmallVec`/`TinyVec` 要加 crate 依赖、增加编译时间。只为"通常 2-3 个元素"就在非热点代码里引入依赖,多半不划算——优先考虑能否直接用数组 `[T; N]` / slice / 返回迭代器。 - **Cow/SmallVec 改返回类型是 API 传染**:把 `-> String` 改成 `-> Cow<'_, str>` 会把生命周期参数传染给所有调用方;把 `Token` 改成 `Token<'src>` 是全 crate 的 API 变更。改公开签名前确认收益配得上波及面;内部 `fn` 则无所谓。 - **量入为出**:Cow/SmallVec 引入复杂度,确认该路径真的是热点或高频才上。非热点的小函数直接 `Vec`/`String` 更清晰。 - **profile 验证**:理论收益不等于实测收益。用 `Criterion` 做微基准,`Flamegraph` 找真热点,别盲改。 **决策速查**: | 情况 | 推荐 | | -------------------- | ------------------------------------------- | | 非热点、简单工具函数 | 直接 `Vec`/`String`/`clone`,别上模式 | | 公开 API 返回类型 | 慎用 `Cow<'_, str>`/生命周期——会传染调用方 | | 真热点 + 内部函数 | 放心上 Cow/SmallVec/借用切片 | | 新增依赖 | 先看能否用 `[T; N]` / 迭代器 / 现有类型替代 |