Strings and Text Processing

Efficient String Construction

Constructing strings in performance-sensitive code avoids repeated heap reallocations and temporary String allocations by pre-allocating capacity, joining slices in a single pass, formatting directly into an existing buffer, or using clone-on-write (Cow<'_, str>).

1. Pre-Allocating with String::with_capacity

When building a String incrementally in a loop with .push() or .push_str(), starting from String::new() triggers multiple geometric reallocations (0 -> 8 -> 16 -> 32 -> ...) as the buffer grows. Pre-allocating with String::with_capacity(estimated_bytes) performs a single allocation:

fn build_csv_row(columns: &[&str]) -> String {
    if columns.is_empty() {
        return String::new();
    }
    let total_bytes: usize = columns.iter().map(|c| c.len()).sum::<usize>() + columns.len() - 1;
    let mut out = String::with_capacity(total_bytes);

    for (i, col) in columns.iter().enumerate() {
        if i > 0 {
            out.push(',');
        }
        out.push_str(col);
    }
    out
}

fn main() {
    let row = build_csv_row(&["1042", "Narayanganj", "active"]);
    println!("Row: '{row}' (len={}, cap={})", row.len(), row.capacity());
}
Row: '1042,Narayanganj,active' (len=23, cap=23)

2. Joining Slices with slice.join(sep) and concat()

When you already have a slice of strings (&[&str] or &[String]), slice.join(sep) and slice.concat() calculate the exact total byte length upfront, allocate the destination String once, and copy all slices in a single pass:

fn main() {
    let parts = ["2026", "10", "23"];
    let date = parts.join("-");
    let compact = parts.concat();

    println!("Joined:  {date}");
    println!("Concat:  {compact}");
}
Joined:  2026-10-23
Concat:  20261023

3. Reusing a Buffer Across Loop Iterations (clear() + write!)

When formatting thousands of lines or records sequentially, allocate a single String outside the loop, format into it with write!(&mut buf, ...), process &buf, and call buf.clear() at the end of each iteration. Because .clear() resets len = 0 while preserving capacity, the entire loop runs with one heap allocation:

use std::fmt::Write;

fn main() {
    let mut scratch = String::with_capacity(64);

    for id in 1..=3 {
        scratch.clear(); // Keeps existing heap capacity!
        write!(&mut scratch, "metric_id={id} status=OK").unwrap();
        println!("Processed: '{scratch}' (cap={})", scratch.capacity());
    }
}
Processed: 'metric_id=1 status=OK' (cap=64)
Processed: 'metric_id=2 status=OK' (cap=64)
Processed: 'metric_id=3 status=OK' (cap=64)

4. Conditional Allocation with Cow<'a, str>

When a function usually returns its input &str unmodified (for example, escaping HTML entities when most strings contain no special characters), return std::borrow::Cow<'a, str> (Cow::Borrowed(s) on the fast path, Cow::Owned(String) only when modification is needed):

use std::borrow::Cow;

fn ensure_https(url: &str) -> Cow<'_, str> {
    if url.starts_with("https://") {
        Cow::Borrowed(url) // Zero allocation!
    } else if let Some(rest) = url.strip_prefix("http://") {
        Cow::Owned(format!("https://{rest}")) // Allocates only when upgrading
    } else {
        Cow::Owned(format!("https://{url}"))
    }
}

fn main() {
    let a = ensure_https("https://rust-lang.org");
    let b = ensure_https("http://example.com");

    println!("a: {a} (borrowed = {})", matches!(a, Cow::Borrowed(_)));
    println!("b: {b} (borrowed = {})", matches!(b, Cow::Borrowed(_)));
}
a: https://rust-lang.org (borrowed = true)
b: https://example.com (borrowed = false)

Summary of String Construction Idioms

Goal Avoid Prefer
Appending formatted values to s: String s.push_str(&format!("{x}")) write!(&mut s, "{x}") (std::fmt::Write)
Joining a slice with a separator Manual loop without with_capacity slice.join(sep)
Concatenating compile-time literals format!("{}{}", "a", "b") concat!("a", "b") (&'static str at compile time)
Returning usually-unchanged text .to_owned() on every call Cow<'a, str>