Loading

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_strlines_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% 性能提升!

关键差异分析

让我们对比两个版本的关键差异:

  1. format! 版本

    mem_str.push_str(format!("{:02X}", *byte).as_str());
    

    这里每次格式化都会:

    • 在堆上分配一个新的 String
    • 格式化字节写入该 String
    • 将该 String 内容复制到 mem_str
    • 丢弃临时字符串
  2. write! 版本

    write!(mem_str, "{:02X}", *byte).unwrap();
    

    这里直接:将格式化的字节写入最终目标 mem_str, 无需任何中间堆分配

当需要处理大量字节并执行大量格式化操作时,这种差异累积起来会产生显著影响。

实际应用场景

这种优化特别适用于以下场景:

  1. 处理大量数据(如内存查看器、十六进制编辑器)
  2. 生成大型报告或日志
  3. 执行高频字符串格式化操作
  4. 资源受限环境(如嵌入式系统)

扩展应用:其他可写目标

除了 Stringwrite! 宏还可以用于任何实现了 std::fmt::Write trait 的类型,包括:

  • 文件(通过 std::io::Write
  • 网络套接字
  • 缓冲区
  • 自定义输出流

总结

在 Rust 中构建字符串时,format!write! 是两种常用的方法,但它们在性能上可能有显著差异:

  1. format!:创建新的格式化字符串,适合需要独立字符串的场景
  2. write!:直接写入目标流,避免中间分配,性能更佳

当需要将多个格式化结果合并到同一个缓冲区时,write! 是明显更好的选择,可以提供高达 75% 的性能提升。这是一个简单但强大的优化技巧,几乎不需要重构代码就能实现显著的性能改进。

下次编写 Rust 代码时,在需要构建大量字符串的场景下,记得考虑使用 write! 代替 format!,这可能会为你的应用程序带来可观的性能提升。

参考文章

  1. write! vs format! when constructing Rust strings by Nicholas Obert, Apr 2025, Medium
posted @ 2025-07-29 23:22  RioTian  阅读(75)  评论(0)    收藏  举报