How do I convert integers to strings fast in Rust?

Walkthrough

The itoa crate provides fast integer-to-string conversion. Unlike std::fmt::Display or ToString::to_string(), itoa avoids heap allocation and is significantly faster for integer formatting. It uses a lookup table and optimized algorithms to convert integers to strings directly into a buffer. This is especially useful in high-performance scenarios like serialization, logging, or any situation where you're converting many integers to strings.

Key concepts:

  1. itoa::Format — the primary trait for integer formatting
  2. IntegerBuffer — stack-allocated buffer for the result
  3. No heap allocation — all work done on the stack
  4. Fast paths — optimized for all integer types
  5. Write trait — integrates with std::fmt::Write

Code Example

# Cargo.toml
[dependencies]
itoa = "1"
use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Convert integer to string
    let s = buffer.format(12345);
    println!("Number: {}", s);
    
    // Works with all integer types
    let s = buffer.format(-98765i32);
    println!("Negative: {}", s);
    
    let s = buffer.format(12345678901234567890u64);
    println!("Large: {}", s);
}

Basic Usage

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Format different integer types
    let positive = buffer.format(42);
    println!("Positive: {}", positive);
    
    let negative = buffer.format(-42);
    println!("Negative: {}", negative);
    
    // Different sizes
    let tiny = buffer.format(0u8);
    println!("Tiny: {}", tiny);
    
    let large = buffer.format(i64::MAX);
    println!("Large: {}", large);
    
    let unsigned = buffer.format(u128::MAX);
    println!("Unsigned: {}", unsigned);
}

Reusing the Buffer

use itoa::Buffer;
 
fn main() {
    // Buffer is reusable - allocate once
    let mut buffer = Buffer::new();
    
    for i in 0..10 {
        // Same buffer used for each conversion
        let s = buffer.format(i);
        print!("{} ", s);
    }
    println!();
    
    // The returned &str is only valid until next format() call
    let s1 = buffer.format(1);
    let s2 = buffer.format(2);
    // s1 is now invalid - don't use it!
    println!("s2: {}", s2);
    
    // Store if you need the string longer
    let s3 = buffer.format(3).to_string(); // Allocates
    let s4 = buffer.format(4);
    println!("s3: {}, s4: {}", s3, s4);
}

All Integer Types

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Signed integers
    let i8_str = buffer.format(127i8);
    println!("i8: {}", i8_str);
    
    let i16_str = buffer.format(32767i16);
    println!("i16: {}", i16_str);
    
    let i32_str = buffer.format(2147483647i32);
    println!("i32: {}", i32_str);
    
    let i64_str = buffer.format(9223372036854775807i64);
    println!("i64: {}", i64_str);
    
    let i128_str = buffer.format(170141183460469231731687303715884105727i128);
    println!("i128: {}", i128_str);
    
    // Unsigned integers
    let u8_str = buffer.format(255u8);
    println!("u8: {}", u8_str);
    
    let u16_str = buffer.format(65535u16);
    println!("u16: {}", u16_str);
    
    let u32_str = buffer.format(4294967295u32);
    println!("u32: {}", u32_str);
    
    let u64_str = buffer.format(18446744073709551615u64);
    println!("u64: {}", u64_str);
    
    let u128_str = buffer.format(340282366920938463463374607431768211455u128);
    println!("u128: {}", u128_str);
    
    // isize and usize
    let isize_str = buffer.format(isize::MAX);
    println!("isize: {}", isize_str);
    
    let usize_str = buffer.format(usize::MAX);
    println!("usize: {}", usize_str);
}

Performance Comparison

use itoa::Buffer;
use std::time::Instant;
 
fn main() {
    const ITERATIONS: u32 = 10_000_000;
    let numbers: Vec<i32> = (0..1000).cycle().take(ITERATIONS as usize).collect();
    
    // Using itoa
    let mut buffer = Buffer::new();
    let start = Instant::now();
    for &n in &numbers {
        let _s = buffer.format(n);
        // Use _s for something to prevent optimization
        std::hint::black_box(_s);
    }
    let itoa_time = start.elapsed();
    
    // Using to_string()
    let start = Instant::now();
    for &n in &numbers {
        let _s = n.to_string();
        std::hint::black_box(_s);
    }
    let to_string_time = start.elapsed();
    
    // Using format!
    let start = Instant::now();
    for &n in &numbers {
        let _s = format!("{}", n);
        std::hint::black_box(_s);
    }
    let format_time = start.elapsed();
    
    println!("itoa:       {:?}", itoa_time);
    println!("to_string:  {:?}", to_string_time);
    println!("format!:    {:?}", format_time);
    println!("itoa speedup vs to_string: {:.2}x", 
             to_string_time.as_secs_f64() / itoa_time.as_secs_f64());
}

Writing to String

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Build a string with multiple numbers
    let mut result = String::new();
    
    for i in 0..5 {
        result.push_str(buffer.format(i));
        if i < 4 {
            result.push(',');
        }
    }
    
    println!("Result: {}", result);
}

Writing to Vec

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    let mut output: Vec<u8> = Vec::new();
    
    // Write numbers as bytes
    let numbers = [1, 20, 300, 4000, 50000];
    
    for &n in &numbers {
        let s = buffer.format(n);
        output.extend_from_slice(s.as_bytes());
        output.push(b' ');
    }
    
    // Convert to string for display
    let result = String::from_utf8(output).unwrap();
    println!("Output: {}", result);
}

JSON-like Formatting

use itoa::Buffer;
 
fn format_json_array(numbers: &[i32]) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::with_capacity(numbers.len() * 8);
    
    result.push('[');
    
    for (i, &n) in numbers.iter().enumerate() {
        if i > 0 {
            result.push(',');
        }
        result.push_str(buffer.format(n));
    }
    
    result.push(']');
    result
}
 
fn main() {
    let numbers = vec![1, 2, 3, 10, 100, 1000];
    let json = format_json_array(&numbers);
    println!("{}", json);
}

CSV Formatting

use itoa::Buffer;
 
fn format_csv_row(values: &[i64]) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::with_capacity(values.len() * 12);
    
    for (i, &value) in values.iter().enumerate() {
        if i > 0 {
            result.push(',');
        }
        result.push_str(buffer.format(value));
    }
    
    result
}
 
fn main() {
    let rows = [
        vec![100, 200, 300],
        vec![400, 500, 600],
        vec![700, 800, 900],
    ];
    
    for row in &rows {
        println!("{}", format_csv_row(row));
    }
}

Custom Number Formatting

use itoa::Buffer;
 
fn format_with_prefix(n: i32, prefix: &str) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::with_capacity(prefix.len() + 11);
    result.push_str(prefix);
    result.push_str(buffer.format(n));
    result
}
 
fn format_with_suffix(n: u64, suffix: &str) -> String {
    let mut buffer = Buffer::new();
    let mut result = String::with_capacity(21 + suffix.len());
    result.push_str(buffer.format(n));
    result.push_str(suffix);
    result
}
 
fn format_percentage(value: i32, total: i32) -> String {
    let mut buffer = Buffer::new();
    let percentage = (value * 100) / total;
    
    let mut result = String::with_capacity(8);
    result.push_str(buffer.format(percentage));
    result.push('%');
    result
}
 
fn main() {
    println!("{}", format_with_prefix(42, "ID-"));
    println!("{}", format_with_suffix(1024, "px"));
    println!("{}", format_percentage(75, 100));
}

Size Prefixed Messages

use itoa::Buffer;
 
fn create_message(id: u32, data: &str) -> Vec<u8> {
    let mut buffer = Buffer::new();
    let mut message = Vec::with_capacity(32 + data.len());
    
    // Message format: "id:length:data"
    message.extend_from_slice(buffer.format(id).as_bytes());
    message.push(b':');
    message.extend_from_slice(buffer.format(data.len()).as_bytes());
    message.push(b':');
    message.extend_from_slice(data.as_bytes());
    
    message
}
 
fn main() {
    let msg = create_message(123, "Hello, World!");
    println!("Message: {:?}", String::from_utf8_lossy(&msg));
    
    // Parse example
    let msg_str = String::from_utf8_lossy(&msg);
    println!("Parsed: {}", msg_str);
}

Buffer Size Information

use itoa::Buffer;
 
fn main() {
    let buffer = Buffer::new();
    
    // Buffer is stack-allocated and sized for max integer
    // u128::MAX is 39 digits, i128::MIN is 40 characters (including sign)
    println!("Buffer size: {} bytes", std::mem::size_of::<Buffer>());
    
    // You can also use heap-allocated buffers if needed
    // But Buffer is efficient for most use cases
    
    // Format the largest possible u128
    let max_u128 = u128::MAX;
    let s = buffer.format(max_u128);
    println!("u128::MAX: {}", s);
    println!("Length: {} characters", s.len());
}

IntegerBuffer Trait

use itoa::{Buffer, Integer};
 
fn format_any_integer<I: Integer>(n: I) -> String {
    let mut buffer = Buffer::new();
    buffer.format(n).to_string()
}
 
fn main() {
    // Works with any integer type
    println!("i8: {}", format_any_integer(127i8));
    println!("u16: {}", format_any_integer(65535u16));
    println!("i32: {}", format_any_integer(-123456i32));
    println!("u64: {}", format_any_integer(9876543210u64));
    println!("i128: {}", format_any_integer(-999999999i128));
}

Streaming to Writer

use itoa::Buffer;
use std::io::{self, Write};
 
fn write_numbers<W: Write>(writer: &mut W, numbers: &[i32]) -> io::Result<()> {
    let mut buffer = Buffer::new();
    
    for (i, &n) in numbers.iter().enumerate() {
        if i > 0 {
            writer.write_all(b"\n")?;
        }
        writer.write_all(buffer.format(n).as_bytes())?;
    }
    
    Ok(())
}
 
fn main() -> io::Result<()> {
    let numbers = vec![1, 2, 3, 4, 5];
    let mut output = Vec::new();
    
    write_numbers(&mut output, &numbers)?;
    
    println!("{}", String::from_utf8_lossy(&output));
    Ok(())
}

Benchmark: Counting Digits

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Count how many digits each number has
    let numbers: Vec<i64> = vec![0, 9, 10, 99, 100, 999, 1000, -1, -10, -100];
    
    for &n in &numbers {
        let s = buffer.format(n);
        let digits = if n < 0 { s.len() - 1 } else { s.len() };
        println!("{} has {} digit(s)", n, digits);
    }
}

Log Formatter

use itoa::Buffer;
use std::io::{self, Write};
 
struct LogWriter<W> {
    writer: W,
    buffer: Buffer,
}
 
impl<W: Write> LogWriter<W> {
    fn new(writer: W) -> Self {
        Self {
            writer,
            buffer: Buffer::new(),
        }
    }
    
    fn log(&mut self, level: &str, code: u32, message: &str) -> io::Result<()> {
        // Write timestamp
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();
        
        self.writer.write_all(b"[")?;
        self.writer.write_all(self.buffer.format(timestamp).as_bytes())?;
        self.writer.write_all(b"] ")?;
        
        // Write level
        self.writer.write_all(level.as_bytes())?;
        self.writer.write_all(b": ")?;
        
        // Write code
        self.writer.write_all(b"[")?;
        self.writer.write_all(self.buffer.format(code).as_bytes())?;
        self.writer.write_all(b"] ")?;
        
        // Write message
        self.writer.write_all(message.as_bytes())?;
        self.writer.write_all(b"\n")?;
        
        Ok(())
    }
}
 
fn main() -> io::Result<()> {
    let mut logger = LogWriter::new(Vec::new());
    
    logger.log("INFO", 200, "Request processed successfully")?;
    logger.log("WARN", 429, "Rate limit exceeded")?;
    logger.log("ERROR", 500, "Internal server error")?;
    
    let output = String::from_utf8_lossy(&logger.writer);
    print!("{}", output);
    
    Ok(())
}

Real-World: CSV Writer

use itoa::Buffer;
use std::io::{self, Write};
 
struct CsvWriter<W> {
    writer: W,
    buffer: Buffer,
    first_field: bool,
}
 
impl<W: Write> CsvWriter<W> {
    fn new(writer: W) -> Self {
        Self {
            writer,
            buffer: Buffer::new(),
            first_field: true,
        }
    }
    
    fn field_i32(&mut self, value: i32) -> io::Result<()> {
        self.write_separator()?;
        self.writer.write_all(self.buffer.format(value).as_bytes())?;
        Ok(())
    }
    
    fn field_u64(&mut self, value: u64) -> io::Result<()> {
        self.write_separator()?;
        self.writer.write_all(self.buffer.format(value).as_bytes())?;
        Ok(())
    }
    
    fn field_str(&mut self, value: &str) -> io::Result<()> {
        self.write_separator()?;
        self.writer.write_all(value.as_bytes())?;
        Ok(())
    }
    
    fn end_row(&mut self) -> io::Result<()> {
        self.writer.write_all(b"\n")?;
        self.first_field = true;
        Ok(())
    }
    
    fn write_separator(&mut self) -> io::Result<()> {
        if !self.first_field {
            self.writer.write_all(b",")?;
        }
        self.first_field = false;
        Ok(())
    }
}
 
fn main() -> io::Result<()> {
    let mut csv = CsvWriter::new(Vec::new());
    
    // Header
    csv.field_str("id")?;
    csv.field_str("age")?;
    csv.field_str("score")?;
    csv.end_row()?;
    
    // Data rows
    csv.field_i32(1)?;
    csv.field_i32(25)?;
    csv.field_u64(950)?;
    csv.end_row()?;
    
    csv.field_i32(2)?;
    csv.field_i32(30)?;
    csv.field_u64(875)?;
    csv.end_row()?;
    
    csv.field_i32(3)?;
    csv.field_i32(28)?;
    csv.field_u64(920)?;
    csv.end_row()?;
    
    let output = String::from_utf8_lossy(&csv.writer);
    print!("{}", output);
    
    Ok(())
}

Real-World: JSON Number Serializer

use itoa::Buffer;
use std::io::{self, Write};
 
struct JsonNumberWriter<W> {
    writer: W,
    buffer: Buffer,
}
 
impl<W: Write> JsonNumberWriter<W> {
    fn new(writer: W) -> Self {
        Self {
            writer,
            buffer: Buffer::new(),
        }
    }
    
    fn write_i32(&mut self, n: i32) -> io::Result<()> {
        self.writer.write_all(self.buffer.format(n).as_bytes())
    }
    
    fn write_u64(&mut self, n: u64) -> io::Result<()> {
        self.writer.write_all(self.buffer.format(n).as_bytes())
    }
    
    fn write_number_array(&mut self, numbers: &[u32]) -> io::Result<()> {
        self.writer.write_all(b"[")?;
        for (i, &n) in numbers.iter().enumerate() {
            if i > 0 {
                self.writer.write_all(b",")?;
            }
            self.writer.write_all(self.buffer.format(n).as_bytes())?;
        }
        self.writer.write_all(b"]")?;
        Ok(())
    }
    
    fn write_key_value(&mut self, key: &str, value: i64) -> io::Result<()> {
        self.writer.write_all(b"\"")?;
        self.writer.write_all(key.as_bytes())?;
        self.writer.write_all(b"\":")?;
        self.writer.write_all(self.buffer.format(value).as_bytes())?;
        Ok(())
    }
}
 
fn main() -> io::Result<()> {
    let mut json = JsonNumberWriter::new(Vec::new());
    
    // Write object
    json.writer.write_all(b"{")?;
    json.write_key_value("count", 42)?;
    json.writer.write_all(b",")?;
    json.write_key_value("total", 123456789)?;
    json.writer.write_all(b"}")?;
    
    println!("{}", String::from_utf8_lossy(&json.writer));
    
    // Write array
    let mut json2 = JsonNumberWriter::new(Vec::new());
    json2.write_number_array(&[1, 2, 3, 4, 5])?;
    println!("{}", String::from_utf8_lossy(&json2.writer));
    
    Ok(())
}

Real-World: ID Generator

use itoa::Buffer;
 
struct IdGenerator {
    prefix: String,
    counter: u64,
    buffer: Buffer,
}
 
impl IdGenerator {
    fn new(prefix: &str) -> Self {
        Self {
            prefix: prefix.to_string(),
            counter: 0,
            buffer: Buffer::new(),
        }
    }
    
    fn next(&mut self) -> String {
        self.counter += 1;
        let mut result = String::with_capacity(self.prefix.len() + 20);
        result.push_str(&self.prefix);
        result.push_str(self.buffer.format(self.counter));
        result
    }
    
    fn current(&self) -> u64 {
        self.counter
    }
}
 
fn main() {
    let mut gen = IdGenerator::new("order-");
    
    println!("ID: {}", gen.next());
    println!("ID: {}", gen.next());
    println!("ID: {}", gen.next());
    
    println!("Generated {} IDs", gen.current());
}

Real-World: Statistics Aggregator

use itoa::Buffer;
use std::collections::BTreeMap;
 
struct Stats {
    counts: BTreeMap<String, u64>,
    buffer: Buffer,
}
 
impl Stats {
    fn new() -> Self {
        Self {
            counts: BTreeMap::new(),
            buffer: Buffer::new(),
        }
    }
    
    fn increment(&mut self, key: &str) {
        *self.counts.entry(key.to_string()).or_insert(0) += 1;
    }
    
    fn record(&mut self, key: &str, value: u64) {
        self.counts.insert(key.to_string(), value);
    }
    
    fn format(&mut self) -> String {
        let mut result = String::with_capacity(self.counts.len() * 20);
        
        let mut first = true;
        for (key, &value) in &self.counts {
            if !first {
                result.push('\n');
            }
            first = false;
            
            result.push_str(key);
            result.push(':');
            result.push_str(self.buffer.format(value));
        }
        
        result
    }
}
 
fn main() {
    let mut stats = Stats::new();
    
    stats.increment("requests");
    stats.increment("requests");
    stats.increment("requests");
    stats.increment("errors");
    
    stats.record("bytes_sent", 12345678);
    stats.record("max_connections", 1000);
    
    println!("{}", stats.format());
}

Comparison: Stack vs Heap

use itoa::Buffer;
 
fn main() {
    // Stack-allocated: Buffer is ~40 bytes
    // No heap allocation for the formatted string
    let mut buffer = Buffer::new();
    for i in 0..1000 {
        let s = buffer.format(i);
        // s is a reference to the stack-allocated buffer
        std::hint::black_box(s);
    }
    
    // Heap-allocated: Each to_string() allocates
    // Higher memory pressure and allocation overhead
    for i in 0..1000 {
        let s = i.to_string();
        // s owns a heap-allocated String
        std::hint::black_box(s);
    }
    
    // When to use each:
    // - Use itoa when you need speed and can reuse the buffer
    // - Use to_string() when you need an owned String
    
    // Demonstration of memory usage
    println!("Buffer size: {} bytes", std::mem::size_of::<Buffer>());
    println!("String size: {} bytes", std::mem::size_of::<String>());
    
    // Buffer is reused, String allocates each time
    println!("\nitoa: Reuse same stack memory");
    println!("to_string: Allocate new heap memory each time");
}

Summary

  • Use Buffer::new() to create a reusable stack-allocated buffer
  • Call buffer.format(integer) to get a &str representation
  • The returned &str is only valid until the next format() call
  • Works with all integer types: i8, i16, i32, i64, i128, isize, and unsigned variants
  • Much faster than to_string() or format!("{}") for integers
  • No heap allocation - all work is done on the stack
  • Buffer is ~40 bytes and can hold any integer's string representation
  • Perfect for: serialization, CSV/JSON writing, logging, ID generation
  • For owned strings, chain .to_string() on the result