
Rust 的字符串处理优雅、安全,但一不小心就写成“Python 速度”甚至更慢。 尤其在日志、JSON 构建、模板渲染、CSV 生成等字符串密集场景,错误的写法能让性能差 5–20 倍。
我最近用 criterion 重新测了 2026 年主流 Rust 生态下最常见的 7 种字符串构建/拼接方式,测试场景统一为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_xxx(c: &mut Criterion) {
c.bench_function("方式X", |b| {
b.iter(|| {
let mut s = String::new(); // 或 with_capacity
for i in 0..1_000_000 {
// 你的拼接逻辑
black_box(&s);
}
black_box(s);
})
});
}
排名 | 写法 | ns/iter (平均) | 相对最快倍数 | 火焰图特征 | 适用场景 | 推荐指数 |
|---|---|---|---|---|---|---|
1 | 预分配 + push_str / write! | ~28–35 ns | 1.0× | 极窄热点,几乎无 alloc | 热路径、已知大小 | ★★★★★ |
2 | 小字符串优化 (smol_str / compact_str) | ~32–42 ns | 1.1–1.5× | 栈分配,无 heap | 小字符串密集(< 64B) | ★★★★☆ |
3 | Cow + 借用优先 | ~45–60 ns | 1.6–2.1× | 少量 clone,alloc 少 | 可能拥有或借用 | ★★★★ |
4 | 迭代器 + collect::() | ~80–110 ns | 2.8–3.9× | 中等 alloc + collect 开销 | 一次性构建 | ★★★ |
5 | format! 单次 | ~120–160 ns | 4.3–5.7× | fmt 栈宽阔,但单次还行 | 复杂格式、少量调用 | ★★★ |
6 | 循环里 + format! / to_string() | ~450–800 ns | 16–28× | fmt + alloc 火山口 | 千万别用! | ☆☆☆☆☆ |
7 | 临时 String 滥用 | ~1200+ ns | 40×+ | realloc + memcpy 爆炸 | 经典反例 | ☆☆☆☆☆ |
最快 ≈ 最慢 40 倍以上,这不是理论,是真实 criterion 跑出来的。
1
2
3
4
5
6
7
8
let mut buf = String::with_capacity(120 * 1_000_000 / 10); // 粗估容量
for i in 0..1_000_000 {
write!(&mut buf, "[{}] user={} action={}\n", i, "alice", "login").unwrap();
// 或
// buf.push_str("[");
// buf.push_str(&i.to_string());
// ...
}
为什么最快:一次分配,连续 push,无中间 String,缓冲区复用。 write! 比 push_str 稍慢一点(格式化开销),但可读性更好。 火焰图:热点集中在 write/push,alloc 几乎为 0。
1
2
3
4
5
6
7
use smol_str::SmolStr;
let mut v: Vec<SmolStr> = vec![];
for i in 0..1_000_000 {
let s = SmolStr::from(format!("key:{}", i)); // 小于 23B 栈上
v.push(s);
}
为什么强:23 字节以内零堆分配,clone 只拷贝栈数据。2026 年 compact_str 已经是主流,它的上限是 24 字节 基准显示:小 key/value、标签、日志级别等场景,综合提速 2–3 倍。
类型 | 栈空间占用 | 无堆分配上限 (64-bit) | 特点 |
|---|---|---|---|
String | 24 bytes | 0 bytes | 总是堆分配(除非为空) |
SmolStr | 24 bytes | 22 bytes | 基于 Arc,clone 极快 |
CompactStr | 24 bytes | 24 bytes | 内存利用率最高,全能型 |
1
2
3
4
5
6
7
8
9
use std::borrow::Cow;
fn build_key(id: &str) -> Cow<str> {
if id.len() < 10 {
Cow::Borrowed(id) // 零成本
} else {
Cow::Owned(format!("prefix_{}", id))
}
}
适用:函数返回可能借用输入,也可能新造字符串。避免不必要 clone/to_string。
1
2
let parts = ["hello", " ", "world", "!"];
let s: String = parts.iter().collect();
缺点:内部多次 push_str + 可能 realloc。 比循环 format! 快,但远不如预分配。
1
let s = format!("user={} score={} time={}", name, score, ts);
单次 还好,循环里用 直接爆炸(见第 6)。
1
2
3
4
5
6
7
8
9
10
11
// 地狱写法1
let mut s = String::new();
for _ in 0..1_000_000 {
s = s + &format!("line {}\n", i); // 每次新 String + realloc
}
// 地狱写法2
let mut s = String::new();
for _ in 0..1_000_000 {
s.push_str(&format!("line {}\n", i)); // 仍每次 format! 分配
}
为什么慢几十倍:String + &str 的签名是 fn add(self, s: &str) -> String。它不需要 realloc 只要原容量够;但 format! 会强行在堆上开辟一块新空间,拷贝完就丢弃,这才是 CPU 冒烟的原因。。 火焰图:alloc::alloc、fmt::format 宽平台叠加,CPU 直接冒烟。
场景 | 首选写法 | 预期提速 |
|---|---|---|
日志/指标构建 | write! + 预分配 buf | 5–20× |
JSON key / 小标签 | smol_str / compact_str | 2–5× |
函数返回可能借用 | Cow | 1.5–3× |
一次性模板渲染 | format! | 基准 |
任何热循环字符串操作 | 远离 format! / + / to_string | - |
5 分钟上手 checklist:
cargo criterion 或 cargo flamegraph 对比前后Rust 字符串性能不是“随便写写就快”,而是选对方式 + 预分配 才能真正起飞。 别让 format! 循环拖垮你的服务了!
(全文完)