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> |