外观
第 7 节:字符串构建——format! 的账要算清
症状
热路径(每请求都执行的代码)里疯狂 format!,程序比预期慢。但有时候 push_str 反而不如 format!——先测再下结论。
诊断
format! 是"拼字符串的瑞士军刀"(第 4 章),但它有个隐藏成本:每次调用都分配一块新内存(第 2 节的分配陷阱)。push_str 是"往已有的字符串上追加"——如果字符串已经 with_capacity 备好容量,追加几乎免费。
实测(10 万次拼接,两种模式):
实测数据
text
dev 模式: format! 42.4 ms push_str 53.0 ms
release 模式:format! 24.1 ms push_str 16.6 ms有意思的现象:
- release 下 push_str 更快(24.1 vs 16.6,约 1.5 倍)——理论优势兑现
- dev 下 format! 反而快——dev 优化不足时,push_str 的多次
to_string()调用开销更大
结论:别迷信"push_str 一定快",热路径要实测(第 8 节)。但量级上,push_str + 预分配是更稳的赢家。
解药
解药一:固定模板拼大文本,用 format!(可读性第一):
rust
let response = format!("HTTP/1.1 {}\r\nContent-Length: {}\r\n\r\n{}", status, length, body);可读性优先,性能足够——冷路径(启动、配置、一次性)用 format! 没毛病。
解药二:循环里反复拼,用 with_capacity + push_str(第 2 节组合拳):
rust
// 慢:每轮 format! 都分配
let mut text = String::new();
for pet in &pets {
text.push_str(&format!("{},", pet.name));
}
// 快:预算容量 + 追加
let mut text = String::with_capacity(pets.len() * 4);
for pet in &pets {
text.push_str(&pet.name);
text.push(',');
}解药三:数字转字符串的坑——format!("{}", n) 每次分配,n.to_string() 也一样;能直接算就绕过字符串:
rust
// 慢:每个数字都造字符串
let total: String = numbers.iter().map(|n| n.to_string()).collect();
// 快:直接求和,一次转字符串
let total = numbers.iter().sum::<u32>().to_string();预防
- 拼一次 → format!(清楚);循环拼 → push_str + with_capacity(快)
writeln!(buffer, ...)是"format 但写到已有字符串":std::fmt::Write的writeln!可以复用缓冲区,是"既要可读性又要复用"的折中- 字符串拼接
+别用:String + &str需要左值可变,还会移动——可读性不如 format!,速度不如 push_str - 又是老规矩:热路径才值得优化,冷路径可读性优先(总纲:先测,再优化)