Rust 性能优化秘籍:write! 宏让字符串构建提速 75%
[!NOTE]
在 Rust 编程中,字符串操作是非常常见的需求。很多开发者习惯使用
format!宏来构建格式化字符串,但你是否知道在某些场景下,使用write!宏可以显著提升性能?本文将分享一个真实案例,展示如何通过简单替换字符串构建方法,实现高达 75% 的性能提升。
问题背景
作者在开发一个用于自定义虚拟机的 GUI 调试器时,需要实现内存查看功能,将程序内存中的每个字节以十六进制格式在网格中显示:
- 每行显示 16 个字节
- 左侧显示行索引(十六进制)
- 右侧显示内存中每个字节的十六进制值
实现这一功能的自然方式是构建两个多行字符串:一个用于左侧的行索引,一个用于右侧的十六进制字节值。
初始实现:使用 format! 宏
作者最初的实现使用了 format! 宏来创建每个字节的十六进制表示:
/// 生成给定内存缓冲区的十六进制内存视图
fn generate_memory_strings(memory: &[u8], mut range: Range<usize>) -> Option<(String, String)> {
const BYTES_PER_MEMORY_ROW: usize = 16;
// 验证请求的字节范围
if memory.len() < range.end {
range.end = memory.len();
if range.start > range.end {
returnNone;
}
}
let mem_view = &memory[range.start..range.end];
// 包含行索引的字符串,用换行符分隔
letmut lines_str = String::new();
// 包含实际内存中十六进制字节的字符串,每行用换行符分隔
letmut mem_str = String::new();
// 内存视图左侧显示的行索引
letmut row_index: usize = range.start;
// 构建字符串
letmut mem_rows = mem_view.array_chunks::<BYTES_PER_MEMORY_ROW>();
for full_row in mem_rows.by_ref() {
for byte in &full_row[..BYTES_PER_MEMORY_ROW-1] {
mem_str.push_str(format!("{:02X}", *byte).as_str());
mem_str.push(' ');
}
mem_str.push_str(format!("{:02X}", full_row[BYTES_PER_MEMORY_ROW-1]).as_str());
mem_str.push('\n');
lines_str.push_str(format!("{:#X}\n", row_index).as_str());
row_index += BYTES_PER_MEMORY_ROW;
}
let remainder_row = mem_rows.remainder();
if !remainder_row.is_empty() {
lines_str.push_str(format!("{:#X}\n", row_index).as_str());
for byte in remainder_row {
mem_str.push_str(format!("{:02X}", *byte).as_str());
mem_str.push(' ');
}
}
Some((
lines_str,
mem_str
))
}
性能瓶颈分析
尽管这种方法能够正常工作,但作者很快注意到,生成大块内存的十六进制视图非常缓慢。即使预先分配两个字符串缓冲区以避免重新分配,性能提升也微不足道。
经过思考,作者发现主要瓶颈是 format! 宏导致的大量堆内存分配。每次调用 format! 时,Rust 都会在堆上分配一个新的 String。在大多数情况下这没问题,但当字符串内容复制到更大的缓冲区(本例中是 mem_str 和 lines_str)后立即丢弃时,这种堆分配就完全没有必要了。
优化方案:使用 write! 宏
write! 宏与 format! 不同,它直接将格式化的字符串写入可写流中,无需中间分配。以下是使用 write! 代替 format! 生成内存视图字符串的优化函数:
fn generate_memory_strings(memory: &[u8], mut range: Range<usize>) -> Option<(String, String)> {
if memory.len() < range.end {
range.end = memory.len();
if range.start > range.end {
returnNone;
}
}
let mem_view = &memory[range.start..range.end];
letmut lines_str = String::new();
letmut mem_str = String::new();
letmut row_index: usize = range.start;
letmut mem_rows = mem_view.array_chunks::<BYTES_PER_MEMORY_ROW>();
for full_row in mem_rows.by_ref() {
for byte in &full_row[..BYTES_PER_MEMORY_ROW-1] {
write!(mem_str, "{:02X} ", *byte).unwrap();
}
writeln!(mem_str, "{:02X}", full_row[BYTES_PER_MEMORY_ROW-1]).unwrap();
writeln!(lines_str, "{:#X}", row_index).unwrap();
row_index += BYTES_PER_MEMORY_ROW;
}
let remainder_row = mem_rows.remainder();
if !remainder_row.is_empty() {
writeln!(lines_str, "{:#X}", row_index).unwrap();
for byte in remainder_row {
write!(mem_str, "{:02X} ", *byte).unwrap();
}
}
Some((
lines_str,
mem_str
))
}
性能对比
使用 Rust 的 test::Bencher 进行基准测试,作者发现通过直接将格式化字符串写入最终缓冲区(而非创建大量临时字符串)实现了惊人的 75% 性能提升!
关键差异分析
让我们对比两个版本的关键差异:
-
format! 版本:
mem_str.push_str(format!("{:02X}", *byte).as_str());这里每次格式化都会:
- 在堆上分配一个新的
String - 格式化字节写入该
String - 将该
String内容复制到mem_str - 丢弃临时字符串
- 在堆上分配一个新的
-
write! 版本:
write!(mem_str, "{:02X}", *byte).unwrap();这里直接:将格式化的字节写入最终目标
mem_str, 无需任何中间堆分配
当需要处理大量字节并执行大量格式化操作时,这种差异累积起来会产生显著影响。
实际应用场景
这种优化特别适用于以下场景:
- 处理大量数据(如内存查看器、十六进制编辑器)
- 生成大型报告或日志
- 执行高频字符串格式化操作
- 资源受限环境(如嵌入式系统)
扩展应用:其他可写目标
除了 String,write! 宏还可以用于任何实现了 std::fmt::Write trait 的类型,包括:
- 文件(通过
std::io::Write) - 网络套接字
- 缓冲区
- 自定义输出流
总结
在 Rust 中构建字符串时,format! 和 write! 是两种常用的方法,但它们在性能上可能有显著差异:
format!宏:创建新的格式化字符串,适合需要独立字符串的场景write!宏:直接写入目标流,避免中间分配,性能更佳
当需要将多个格式化结果合并到同一个缓冲区时,write! 是明显更好的选择,可以提供高达 75% 的性能提升。这是一个简单但强大的优化技巧,几乎不需要重构代码就能实现显著的性能改进。
下次编写 Rust 代码时,在需要构建大量字符串的场景下,记得考虑使用 write! 代替 format!,这可能会为你的应用程序带来可观的性能提升。

浙公网安备 33010602011771号