Loading page…
Rust walkthroughs
Loading page…
itoa::fmt and std::write! for integer formatting in performance-critical code?itoa::fmt provides specialized integer-to-string formatting that writes directly to an output buffer without intermediate allocations, while std::write! uses the standard Display trait infrastructure which involves more abstraction layers and runtime dispatch. The itoa crate achieves performance by pre-computing lookup tables for digit conversion, avoiding dynamic trait dispatch, and writing digits directly to the target buffer in reverse order (since integers are converted from least significant digit upward but must be written most-significant-first). The trade-off is API flexibility: itoa::fmt works only with integers and fmt::Formatter or fmt::Write targets, while std::write! supports any type implementing Display and can write to any type implementing fmt::Write with string interpolation and formatting options like width and precision.
use std::fmt::Write;
fn format_with_itoa(value: i32, buffer: &mut String) {
// itoa::fmt writes directly to the buffer
itoa::fmt(buffer, value).unwrap();
}
fn format_with_write(value: i32, buffer: &mut String) {
// std::write! uses Display trait
write!(buffer, "{}", value).unwrap();
}
fn format_with_display(value: i32, buffer: &mut String) {
// std::fmt::Write::write_fmt uses the same Display trait
buffer.write_fmt(format_args!("{}", value)).unwrap();
}
fn main() {
let mut buffer1 = String::new();
let mut buffer2 = String::new();
format_with_itoa(12345, &mut buffer1);
format_with_write(12345, &mut buffer2);
assert_eq!(buffer1, buffer2);
println!("Both produce: {}", buffer1);
}Both approaches produce identical output; the difference is in how they achieve it.
use std::fmt::Write;
fn main() {
// itoa::fmt is specialized for integers
let mut buffer = String::new();
// Format directly to buffer
itoa::fmt(&mut buffer, 42).unwrap();
println!("itoa result: {}", buffer);
// Works with all integer types
let mut buffer = String::new();
itoa::fmt(&mut buffer, 255u8).unwrap();
println!("u8: {}", buffer);
let mut buffer = String::new();
itoa::fmt(&mut buffer, -12345i32).unwrap();
println!("i32: {}", buffer);
let mut buffer = String::new();
itoa::fmt(&mut buffer, 9_223_372_036_854_775_807i64).unwrap();
println!("i64: {}", buffer);
// Works with usize/isize
let mut buffer = String::new();
itoa::fmt(&mut buffer, 1000usize).unwrap();
println!("usize: {}", buffer);
}itoa::fmt accepts only integer types and writes them directly to a fmt::Write target.
use std::fmt::Write;
fn main() {
let mut buffer = String::new();
// write! is a general-purpose formatting macro
write!(buffer, "{}", 42).unwrap();
println!("write! result: {}", buffer);
// It supports any Display type
let mut buffer = String::new();
write!(buffer, "{}", "hello").unwrap();
println!("String: {}", buffer);
// It supports formatting options
let mut buffer = String::new();
write!(buffer, "{:05}", 42).unwrap(); // Zero-padded, width 5
println!("Padded: {}", buffer);
// It supports interpolation
let mut buffer = String::new();
write!(buffer, "value = {}, hex = {:x}", 255, 255).unwrap();
println!("Interpolation: {}", buffer);
// Works with custom Display types
struct Point { x: i32, y: i32 }
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
let mut buffer = String::new();
write!(buffer, "{}", Point { x: 1, y: 2 }).unwrap();
println!("Custom: {}", buffer);
}write! is general-purpose and supports any Display type with formatting options.
use std::fmt::Write;
use std::time::Instant;
fn main() {
const COUNT: usize = 1_000_000;
let numbers: Vec<i32> = (0..COUNT as i32).collect();
// itoa::fmt approach
let start = Instant::now();
let mut buffer = String::new();
for &n in &numbers {
buffer.clear();
itoa::fmt(&mut buffer, n).unwrap();
// Use buffer...
}
let itoa_duration = start.elapsed();
// std::write! approach
let start = Instant::now();
let mut buffer = String::new();
for &n in &numbers {
buffer.clear();
write!(buffer, "{}", n).unwrap();
// Use buffer...
}
let write_duration = start.elapsed();
// std::fmt::Write trait approach
let start = Instant::now();
let mut buffer = String::new();
for &n in &numbers {
buffer.clear();
buffer.write_fmt(format_args!("{}", n)).unwrap();
// Use buffer...
}
let trait_duration = start.elapsed();
println!("itoa::fmt: {:?}", itoa_duration);
println!("write!: {:?}", write_duration);
println!("trait: {:?}", trait_duration);
// Typical results show itoa::fmt is 2-5x faster
// The difference is more pronounced with:
// - More iterations
// - Tighter loops
// - More integer formatting relative to other work
}itoa::fmt avoids the overhead of the Display trait machinery.
// The itoa crate achieves speed through:
// 1. SPECIALIZATION: Only handles integers, no trait dispatch
// itoa::fmt signature is specialized for integers:
// pub fn fmt<W: fmt::Write, I: Integer>(writer: W, value: I) -> fmt::Result
// 2. LOOKUP TABLES: Pre-computed digit tables
// Instead of computing digit characters at runtime:
// let digit = (value % 10) as u8 + b'0'; // computed each time
// itoa uses pre-computed tables for faster conversion
// 3. DIRECT BUFFER WRITING: Writes digits in reverse
// Integer conversion generates digits from least significant
// but output needs most significant first
// itoa handles this efficiently without intermediate strings
// 4. NO FORMATTING OPTIONS: No width, precision, etc.
// Skip parsing format strings and applying formatting
// 5. INLINED CODE: Core conversion is fully inlined
// The compiler can optimize the entire conversion path
fn main() {
// Demonstrate: itoa::fmt has NO formatting options
let mut buffer = String::new();
itoa::fmt(&mut buffer, 42).unwrap();
println!("Simple integer: {}", buffer);
// No way to do:
// itoa::fmt(&mut buffer, format_args!("{:05}", 42))
// itoa doesn't support format strings
// If you need formatting, use write!
let mut buffer = String::new();
write!(buffer, "{:05}", 42).unwrap();
println!("With padding: {}", buffer);
}itoa trades flexibility for speed by specializing exclusively for integers.
use std::fmt::Write;
fn main() {
// USE CASE 1: Serializing many integers to JSON
fn write_json_numbers_itoa(values: &[i32]) -> String {
let mut result = String::new();
result.push('[');
for (i, &v) in values.iter().enumerate() {
if i > 0 {
result.push(',');
}
itoa::fmt(&mut result, v).unwrap();
}
result.push(']');
result
}
fn write_json_numbers_write(values: &[i32]) -> String {
let mut result = String::new();
result.push('[');
for (i, &v) in values.iter().enumerate() {
if i > 0 {
result.push(',');
}
write!(result, "{}", v).unwrap();
}
result.push(']');
result
}
let values: Vec<i32> = (0..100).collect();
let json1 = write_json_numbers_itoa(&values);
let json2 = write_json_numbers_write(&values);
assert_eq!(json1, json2);
println!("JSON output: {}", &json1[..50]);
// USE CASE 2: High-performance logging
fn log_value_itoa(value: i64, buffer: &mut String) {
buffer.clear();
itoa::fmt(buffer, value).unwrap();
// Now buffer contains the string representation
// No allocation for the conversion itself
}
// USE CASE 3: CSV output
fn write_csv_row_itoa(numbers: &[i64], buffer: &mut String) {
buffer.clear();
for (i, &n) in numbers.iter().enumerate() {
if i > 0 {
buffer.push(',');
}
itoa::fmt(buffer, n).unwrap();
}
}
let mut buffer = String::new();
write_csv_row_itoa(&[1, 2, 3, 4, 5], &mut buffer);
println!("CSV row: {}", buffer);
}Use itoa::fmt when formatting many integers in tight loops or serialization.
use std::fmt::Write;
fn main() {
// USE CASE 1: Formatting options needed
let mut buffer = String::new();
write!(buffer, "{:05}", 42).unwrap(); // "00042"
println!("Padded: {}", buffer);
let mut buffer = String::new();
write!(buffer, "{:x}", 255).unwrap(); // "ff"
println!("Hex: {}", buffer);
let mut buffer = String::new();
write!(buffer, "{:#x}", 255).unwrap(); // "0xff"
println!("Hex with prefix: {}", buffer);
let mut buffer = String::new();
write!(buffer, "{:b}", 10).unwrap(); // "1010"
println!("Binary: {}", buffer);
// USE CASE 2: Mixed content formatting
fn format_summary(name: &str, id: u64, score: f64) -> String {
let mut buffer = String::new();
write!(buffer, "Name: {}, ID: {}, Score: {:.2}", name, id, score).unwrap();
buffer
}
println!("{}", format_summary("Alice", 12345, 98.7654));
// USE CASE 3: Readability matters more than performance
fn debug_output(count: usize, total: i64) -> String {
let mut buffer = String::new();
write!(buffer, "Processed {} items, total: {}", count, total).unwrap();
buffer
}
println!("{}", debug_output(100, 12345));
// USE CASE 4: Non-integer types
fn format_point(x: f64, y: f64) -> String {
let mut buffer = String::new();
write!(buffer, "({}, {})", x, y).unwrap();
buffer
}
println!("{}", format_point(1.5, 2.5));
}Use write! when you need formatting options, mixed types, or readability is priority.
use std::fmt::Write;
fn main() {
// Both approaches need buffer space
// Maximum digits for each type:
// - u8: 3 digits (255)
// - u16: 5 digits (65535)
// - u32: 10 digits (4294967295)
// - u64: 20 digits (18446744073709551615)
// - i64: 20 digits (-9223372036854775808)
// itoa::fmt writes to existing buffer
let mut buffer = String::with_capacity(20); // Reusable
for i in 0..1000 {
buffer.clear();
itoa::fmt(&mut buffer, i).unwrap();
// Buffer is reused, no new allocation
}
// write! also writes to existing buffer
let mut buffer = String::with_capacity(20);
for i in 0..1000 {
buffer.clear();
write!(buffer, "{}", i).unwrap();
// Buffer is reused similarly
}
// Key difference: write! has more overhead even with same buffer
// Alternative: itoa::Integer::MAX_STR_LENGTH for stack arrays
const BUFFER_SIZE: usize = 20; // Max for i64/u64
fn format_to_stack_array(value: i64) -> [u8; BUFFER_SIZE] {
let mut buffer = [0u8; BUFFER_SIZE];
// itoa can write to byte arrays too (via Integer trait)
let s = itoa::Buffer::new();
let formatted = s.format(value);
let bytes = formatted.as_bytes();
let len = bytes.len();
let mut result = [0u8; BUFFER_SIZE];
result[..len].copy_from_slice(bytes);
result
}
let arr = format_to_stack_array(12345);
println!("Stack array: {:?}", &arr[..5]);
}Both can reuse buffers; the difference is in conversion overhead, not allocation.
fn main() {
// itoa::Buffer provides stack-allocated formatting
let mut buffer = itoa::Buffer::new();
let s = buffer.format(12345);
println!("Formatted: {}", s);
// Buffer::new creates a stack-allocated buffer
// No heap allocation at all
// You can reuse the buffer:
let mut buffer = itoa::Buffer::new();
for i in [0, 100, -999, 12345678] {
let s = buffer.format(i);
println!("{} -> {}", i, s);
}
// This is the most performant way to format integers
// The buffer lives on stack and is reused
// Comparison of allocation patterns:
// 1. String with write! (heap allocation)
let mut s = String::new();
write!(s, "{}", 12345).unwrap(); // May allocate on String
// 2. String with itoa::fmt (heap allocation)
let mut s = String::new();
itoa::fmt(&mut s, 12345).unwrap(); // String allocates
// 3. itoa::Buffer (stack allocation)
let mut buf = itoa::Buffer::new();
let s = buf.format(12345); // No heap allocation
// Buffer::format returns &str that borrows from the Buffer
// The Buffer is typically [u8; MAX_INT_LENGTH]
}itoa::Buffer::new() creates a stack-allocated buffer for zero-allocation formatting.
use std::fmt::Write;
use std::time::Instant;
fn benchmark_integer_formatting() {
const COUNT: usize = 10_000_000;
let numbers: Vec<i32> = (0..COUNT as i32).collect();
// Test 1: itoa::Buffer (stack, zero allocation)
let start = Instant::now();
let mut buf = itoa::Buffer::new();
let mut sum = 0u64;
for &n in &numbers {
let s = buf.format(n);
sum += s.len() as u64;
}
let buffer_duration = start.elapsed();
println!("itoa::Buffer: {:?}, sum: {}", buffer_duration, sum);
// Test 2: itoa::fmt to String (heap allocation)
let start = Instant::now();
let mut string = String::new();
let mut sum = 0u64;
for &n in &numbers {
string.clear();
itoa::fmt(&mut string, n).unwrap();
sum += string.len() as u64;
}
let fmt_duration = start.elapsed();
println!("itoa::fmt: {:?}, sum: {}", fmt_duration, sum);
// Test 3: write! to String (heap allocation)
let start = Instant::now();
let mut string = String::new();
let mut sum = 0u64;
for &n in &numbers {
string.clear();
write!(string, "{}", n).unwrap();
sum += string.len() as u64;
}
let write_duration = start.elapsed();
println!("write!: {:?}, sum: {}", write_duration, sum);
// Test 4: to_string (creates new String each time)
let start = Instant::now();
let mut sum = 0u64;
for &n in &numbers {
let s = n.to_string();
sum += s.len() as u64;
}
let tostring_duration = start.elapsed();
println!("to_string: {:?}, sum: {}", tostring_duration, sum);
// Typical results (your numbers will vary):
// itoa::Buffer: fastest (stack, zero allocation)
// itoa::fmt: faster than write! (specialized)
// write!: slower than itoa (generalized)
// to_string: slowest (allocation per iteration)
}
fn main() {
benchmark_integer_formatting();
}The performance difference is most visible in tight loops with many integers.
fn main() {
// Understanding the memory paths:
// write! macro expansion (conceptual):
// write!(buffer, "{}", value)
// becomes something like:
// buffer.write_fmt(format_args!("{}", value))
// which calls Display::fmt on value
// which goes through trait dispatch
// which converts digits and writes to buffer
// itoa::fmt expansion (conceptual):
// itoa::fmt(buffer, value)
// directly calls specialized integer conversion
// uses lookup tables for digit characters
// writes directly to buffer without trait dispatch
// The key differences:
// 1. Trait dispatch vs direct call
// 2. Format string parsing vs none
// 3. Generalized vs specialized code path
// This is why itoa::fmt is faster for integers specifically
// But write! can handle any Display type
}write! goes through generic trait dispatch; itoa::fmt calls specialized code directly.
use std::fmt::Write;
fn main() {
// RECOMMENDATION MATRIX:
// Need formatting options (padding, hex, etc.)?
// -> Use write! (itoa doesn't support these)
// Need to mix integers with other text?
// -> Use write! (simpler, readable)
// Formatting millions of integers in a tight loop?
// -> Use itoa::Buffer::new().format()
// Serializing integers to JSON/CSV in hot path?
// -> Use itoa::fmt
// Writing integers to a reused String buffer?
// -> Use itoa::fmt for speed, write! for simplicity
// One-off integer to string?
// -> Use .to_string() or write! (readability)
// Example decision process:
fn format_single(id: u64) -> String {
// One-off: readability wins
format!("ID-{}", id)
}
fn format_many(values: &[i64]) -> Vec<String> {
// Many conversions: performance matters
values.iter()
.map(|&v| {
let mut buf = itoa::Buffer::new();
buf.format(v).to_string()
})
.collect()
}
fn format_to_buffer(values: &[i64], output: &mut String) {
// Reusing buffer: use itoa::fmt
output.clear();
for (i, &v) in values.iter().enumerate() {
if i > 0 {
output.push(',');
}
itoa::fmt(output, v).unwrap();
}
}
fn format_with_padding(value: i32) -> String {
// Need padding: must use write!
let mut buffer = String::new();
write!(buffer, "{:08}", value).unwrap();
buffer
}
let formatted = format_with_padding(42);
println!("Padded: {}", formatted); // "00000042"
}Choose based on the balance of performance needs, formatting requirements, and code clarity.
Performance comparison (relative, approximate):
| Method | Allocation | Trait Dispatch | Format Parsing | Speed |
|--------|------------|----------------|---------------|-------|
| itoa::Buffer::new().format() | Stack only | None | None | Fastest |
| itoa::fmt(&mut string, value) | Heap (String) | None | None | Fast |
| write!(string, "{}", value) | Heap (String) | Display | Some | Medium |
| value.to_string() | Heap (new String) | Display | Some | Slowest |
Capability comparison:
| Feature | itoa::fmt | write! |
|---------|-------------|----------|
| Integer types | All | All |
| Non-integer types | No | Yes (Display) |
| Format options (width) | No | Yes |
| Format options (hex) | No | Yes |
| Format options (padding) | No | Yes |
| String interpolation | No | Yes |
| Trait dispatch | None | Yes |
| Zero allocation | Buffer::new() | No |
When to use itoa::fmt:
When to use write!:
Key insight: The choice between itoa::fmt and std::write! represents a classic performance-versus-flexibility trade-off. The itoa crate specializes entirely in integer-to-string conversion, eliminating trait dispatch, format string parsing, and using pre-computed lookup tables. This makes it 2-5x faster than write! for integer formatting. However, itoa cannot format floats, strings, or custom types; it doesn't support hex, binary, padding, or any formatting options. When you need those features, write! (or format!, to_string()) is your only option. The most performant approach is itoa::Buffer::new().format(value) which returns a &str borrowing from a stack-allocated buffer—truly zero heap allocation. For practical code, consider your bottleneck: if integer formatting is a small fraction of total work, the readability of write! may be worth the overhead. If you're serializing millions of integers or formatting in tight loops, itoa provides measurable speedups with minimal complexity.