Loading page…
Rust walkthroughs
Loading page…
itoa::IntegerToString and when would you use it instead of std::fmt::Display?The itoa crate provides optimized integer-to-string conversion that outperforms std::fmt::Display by a significant margin. While Display is designed for general-purpose formatting with allocation overhead, itoa focuses solely on integer conversion with specialized algorithms that avoid the overhead of the formatting infrastructure.
fn display_approach() -> String {
// std::fmt::Display is general-purpose
let num: i64 = 12345;
let s = num.to_string();
s
}
fn display_performance() {
// Display works through the fmt machinery:
// 1. Creates a formatter
// 2. Writes to a String buffer
// 3. Handles various formatting options
// 4. Has branching for all integer types
let mut result = String::new();
for i in 0..1_000_000 {
result.push_str(&i.to_string());
}
// This is slow because each to_string() goes through
// the full formatting machinery
}Display is designed for flexibility, not raw integer conversion speed.
use itoa::IntegerToString;
fn basic_itoa() {
// itoa::IntegerToString provides fast conversion
let num: i64 = 12345;
let s = num.to_string();
println!("{}", s); // "12345"
// Works with all integer types
let small: i32 = 42;
let big: u64 = 9876543210;
let byte: u8 = 255;
let neg: i32 = -100;
println!("{}", small.to_string()); // "42"
println!("{}", big.to_string()); // "9876543210"
println!("{}", byte.to_string()); // "255"
println!("{}", neg.to_string()); // "-100"
}IntegerToString provides a to_string() method, shadowing the standard library's.
use itoa::Buffer;
fn buffer_usage() {
// Buffer avoids allocation entirely
let mut buffer = Buffer::new();
let num: i64 = 12345;
let s: &str = buffer.format(num);
println!("{}", s); // "12345"
// The string lives in the buffer, not a new allocation
// This is the fastest way to convert integers to strings
}Buffer::format() returns a &str referencing the buffer's internal storage.
use itoa::Buffer;
fn reuse_buffer() {
let mut buffer = Buffer::new();
// Reuse the same buffer for multiple conversions
for i in 0..10 {
let s = buffer.format(i);
println!("{}", s);
}
// No allocations during the loop
// The buffer is reused each time
}
fn collect_with_buffer() -> Vec<String> {
let mut buffer = Buffer::new();
let mut results = Vec::new();
for i in 0..1000 {
let s = buffer.format(i);
results.push(s.to_owned()); // Only allocate for the Vec entries
}
results
}Reusing a buffer eliminates all allocation overhead for the conversion itself.
use itoa::Buffer;
fn performance_comparison() {
let count = 1_000_000;
// Using std::fmt::Display
use std::time::Instant;
let start = Instant::now();
let mut display_results = String::new();
for i in 0..count {
display_results.push_str(&i.to_string());
}
let display_duration = start.elapsed();
// Using itoa::IntegerToString (allocating)
let start = Instant::now();
let mut itoa_results = String::new();
for i in 0..count {
itoa_results.push_str(&IntegerToString::to_string(&i));
}
let itoa_alloc_duration = start.elapsed();
// Using itoa::Buffer (no allocation)
let start = Instant::now();
let mut buffer = Buffer::new();
let mut buffer_results = String::new();
for i in 0..count {
buffer_results.push_str(buffer.format(i));
}
let buffer_duration = start.elapsed();
println!("Display: {:?}", display_duration);
println!("itoa (alloc): {:?}", itoa_alloc_duration);
println!("itoa (buffer): {:?}", buffer_duration);
// Typical results:
// Display: ~300ms
// itoa (alloc): ~150ms (2x faster)
// itoa (buffer): ~50ms (6x faster)
}The buffer approach is significantly faster because it avoids allocation.
use itoa::Buffer;
fn how_itoa_works() {
// itoa uses specialized algorithms:
//
// 1. Pre-computed digit tables
// 2. Division by 100 (not 10) for fewer operations
// 3. Direct digit writing without formatting machinery
// 4. Branch-free paths where possible
// 5. Size-optimized lookup tables for each integer type
let mut buffer = Buffer::new();
// The buffer is sized to hold the maximum integer:
// - i64: up to 20 digits + sign = 21 bytes
// - i32: up to 11 digits + sign = 12 bytes
let max_i64: i64 = i64::MAX;
let s = buffer.format(max_i64);
println!("Max i64: {} ({} chars)", s, s.len());
// Max i64: 9223372036854775807 (19 chars)
let min_i64: i64 = i64::MIN;
let s = buffer.format(min_i64);
println!("Min i64: {} ({} chars)", s, s.len());
// Min i64: -9223372036854775808 (20 chars)
}itoa uses algorithmic optimizations specific to integer conversion.
use itoa::{Buffer, IntegerToString};
fn use_itoa_when() {
// Use itoa when:
// 1. Converting many integers in a hot loop
let mut buffer = Buffer::new();
let mut output = String::new();
for i in 0..1_000_000 {
output.push_str(buffer.format(i));
output.push('\n');
}
// 2. Writing to output streams
use std::io::Write;
let stdout = std::io::stdout();
let mut handle = stdout.lock();
for i in 0..1000 {
write!(handle, "{}", buffer.format(i)).unwrap();
}
// 3. Building delimited strings
let nums: Vec<i32> = (0..100).collect();
let mut csv = String::new();
for (i, num) in nums.iter().enumerate() {
if i > 0 { csv.push(','); }
csv.push_str(buffer.format(*num));
}
// 4. Any performance-sensitive integer output
}
fn use_display_when() {
// Use Display when:
// 1. General formatting with options
let num = 42;
let s1 = format!("{:05}", num); // "00042" - padding
let s2 = format!("{:+}", num); // "+42" - sign
let s3 = format!("{:#x}", num); // "0x2a" - hex
let s4 = format!("{:b}", num); // "101010" - binary
// 2. Debug/logging output (not performance critical)
println!("Value: {}", num);
// 3. User-facing messages
let message = format!("You have {} items", num);
// 4. Rare conversions (not worth the dependency)
let one_off = 123.to_string();
}Choose based on whether you need formatting options or raw speed.
use itoa::Buffer;
use std::io::{self, Write};
fn write_integers() -> io::Result<()> {
let mut buffer = Buffer::new();
let stdout = io::stdout();
let mut handle = stdout.lock();
// Write integers without allocation
for i in 0..1000 {
handle.write_all(buffer.format(i).as_bytes())?;
handle.write_all(b"\n")?;
}
Ok(())
}
fn write_to_file() -> std::io::Result<()> {
use std::fs::File;
let mut file = File::create("numbers.txt")?;
let mut buffer = Buffer::new();
for i in 0..100_000 {
file.write_all(buffer.format(i).as_bytes())?;
file.write_all(b",")?;
}
Ok(())
}itoa integrates naturally with Write for efficient output.
use itoa::IntegerToString;
fn trait_usage() {
// IntegerToString is implemented for all integer types
fn convert<T: IntegerToString>(n: T) -> String {
n.to_string()
}
// Works with any integer
let a: String = convert(42i32);
let b: String = convert(42u64);
let c: String = convert(42isize);
let d: String = convert(42usize);
// The trait provides:
// fn to_string(&self) -> String
//
// Note: This shadows std::ToString but is more efficient
}The trait works uniformly across all integer types.
use itoa::Buffer;
fn buffer_lifetimes() {
let mut buffer = Buffer::new();
// The returned &str borrows from buffer
let s1: &str = buffer.format(123);
// Using s1 is fine
println!("{}", s1);
// But this invalidates s1:
let s2: &str = buffer.format(456);
// s1 is no longer valid!
// This would be a compile error:
// println!("{}", s1); // s1 borrowed mutably
// Correct pattern:
let s = buffer.format(789);
println!("{}", s); // Use immediately
// Or copy if you need to keep it:
let owned: String = buffer.format(999).to_owned();
// owned is independent of buffer
}The returned &str is valid until the next format() call.
use itoa::Buffer;
fn large_integers() {
let mut buffer = Buffer::new();
// i64/u64 values
let large: u64 = 18_446_744_073_709_551_615;
let s = buffer.format(large);
println!("u64 max: {}", s);
// i128/u128 (if feature enabled)
// Note: itoa supports i128/u128 with "i128" feature
let huge: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;
// let s = buffer.format(huge); // Works with i128 feature
}The buffer is sized for maximum integer length.
use itoa::Buffer;
fn output_equivalence() {
let mut buffer = Buffer::new();
// itoa and Display produce identical output
let test_values: &[i64] = &[
0, 1, -1, 42, -42,
123456789, -123456789,
i64::MAX, i64::MIN,
];
for &val in test_values {
let itoa_output = buffer.format(val);
let display_output = val.to_string();
assert_eq!(itoa_output, display_output,
"Mismatch for {}: itoa={}, display={}",
val, itoa_output, display_output);
}
println!("All outputs are identical!");
}itoa produces the same output as Display, just faster.
use itoa::Buffer;
use std::io::Write;
struct JsonNumberWriter<W: Write> {
writer: W,
buffer: Buffer,
}
impl<W: Write> JsonNumberWriter<W> {
fn new(writer: W) -> Self {
Self {
writer,
buffer: Buffer::new()
}
}
fn write_number(&mut self, n: i64) -> std::io::Result<()> {
self.writer.write_all(self.buffer.format(n).as_bytes())
}
fn write_array(&mut self, numbers: &[i64]) -> std::io::Result<()> {
self.writer.write_all(b"[")?;
for (i, &n) in numbers.iter().enumerate() {
if i > 0 {
self.writer.write_all(b",")?;
}
self.write_number(n)?;
}
self.writer.write_all(b"]")?;
Ok(())
}
}
fn json_example() -> std::io::Result<()> {
let mut writer = JsonNumberWriter::new(Vec::new());
writer.write_array(&[1, 2, 3, 4, 5])?;
let result = String::from_utf8(writer.writer).unwrap();
println!("{}", result); // [1,2,3,4,5]
Ok(())
}itoa is ideal for custom serialization formats.
use itoa::Buffer;
use std::time::Instant;
fn benchmark_itoa() {
let iterations = 10_000_000;
// Warm up
let mut buffer = Buffer::new();
for i in 0..1000 {
let _ = buffer.format(i);
let _ = i.to_string();
}
// Benchmark itoa with buffer
let start = Instant::now();
let mut buffer = Buffer::new();
for i in 0..iterations {
std::hint::black_box(buffer.format(i));
}
let itoa_time = start.elapsed();
// Benchmark Display
let start = Instant::now();
for i in 0..iterations {
std::hint::black_box(i.to_string());
}
let display_time = start.elapsed();
println!("itoa (buffer): {:?}", itoa_time);
println!("Display: {:?}", display_time);
println!("Speedup: {:.1}x",
display_time.as_nanos() as f64 / itoa_time.as_nanos() as f64);
// Typical results:
// itoa (buffer): ~50ms
// Display: ~300ms
// Speedup: ~6x
}Real benchmarks show 5-10x speedups for integer conversion.
// Note: itoa is for integers only
// For floating-point, use dtoa crate (same author)
// dtoa provides similar functionality for f32/f64
// fn float_conversion() {
// use dtoa::Buffer;
// let mut buffer = Buffer::new();
// let s = buffer.format(3.14159);
// println!("{}", s); // "3.14159"
// }The sister crate dtoa provides similar functionality for floating-point numbers.
itoa::IntegerToString and Buffer provide fast integer-to-string conversion optimized for performance-critical code:
When to use itoa:
When to use std::fmt::Display:
Performance characteristics:
| Method | Allocations | Speed |
|--------|-------------|-------|
| i.to_string() (Display) | 1 per call | Baseline |
| IntegerToString::to_string() | 1 per call | ~2x faster |
| Buffer::format(i) | 0 | ~6x faster |
Key API differences:
use itoa::{Buffer, IntegerToString};
// Allocation per call
let s: String = 42.to_string(); // Display
let s: String = IntegerToString::to_string(&42); // itoa, faster
// No allocation
let mut buffer = Buffer::new();
let s: &str = buffer.format(42); // itoa, fastest
// s is valid until next format() callThe itoa crate demonstrates that specializing for a specific use case (integer-to-string) can yield significant performance improvements over general-purpose formatting infrastructure.