外观
第 2 节:循环里的分配——隐形杀手
症状
代码逻辑完全正确,就是"莫名慢"。尤其在循环体里 String::new()、Vec::new()、format!——每轮循环都在偷偷"进货"。
诊断
String/Vec 是"能长大"的容器(第 7 章):它们背后有容量的概念。每次 push 到容量上限,就要重新分配一块更大的内存 + 把旧内容搬过去——这个操作叫"扩容"。扩容的规律是"翻倍",所以反复 push 会导致反复扩容、反复搬家。
实测(循环 push 100,000 次,release 模式):
实测数据
text
String 无预算:358.6 µs
String 有预算:159.3 µs
Vec 无预算:235.9 µs
Vec 有预算:91.1 µs预先知道容量,with_capacity 一次到位,快 2~3 倍——而且省下的内存更可观(扩容会留"空余容量")。
解药
解药一:知道上限,就 with_capacity:
rust
// 慢:String::new() 从零开始,反复扩容搬家
let mut result = String::new();
for i in 0..100_000 {
result.push_str("abc");
}
// 快:一次把容量备够(你知道总长大概是 300_000)
let mut result = String::with_capacity(300_000);
for i in 0..100_000 {
result.push_str("abc");
}Vec 同理:
rust
let mut items = Vec::with_capacity(100_000); // 知道要装 10 万个
for i in 0..100_000 {
items.push(i);
}解药二:循环里别 String::new() 再造,复用同一个——每轮 push 前 clear() 清空(容量还在):
rust
// 慢:每轮造新字符串
for chunk in data.chunks(100) {
let mut line = String::new();
line.push_str(&format!("{:?}", chunk));
write_line(&line);
}
// 快:一个字符串反复用,clear 只清内容不清容量
let mut line = String::new();
for chunk in data.chunks(100) {
line.clear();
line.push_str(&format!("{:?}", chunk));
write_line(&line);
}解药三:一行 API 能替代循环的,用 API(第 3 节迭代器)——collect 内部会智能预分配:
rust
let numbers: Vec<u32> = (0..100_000).map(|i| i * 2).collect(); // 内部 with_capacity预防
- 循环体里看到
String::new()/Vec::new()/format!,先警惕:它每轮都在分配 - "结果要装进一个大集合" →
with_capacity(或让collect干) - "同一个容器反复用" → 循环外建,循环内
clear() - 内存翻倍扩容的教训:
Vec装 1,000,000 个时可能分配过 1,048,576 × 2 的内存——容量是"下一个 2 的幂",别惊讶内存占用比 len 大