How do I convert integers to strings fast with itoa in Rust?

Walkthrough

The itoa crate provides a fast integer-to-string conversion library that outperforms the standard library's ToString trait and format! macro for integer types. It achieves this speed by using carefully optimized assembly and SIMD instructions on supported platforms. The crate offers a simple API with the Integer trait that provides the to_string() method, but with significantly better performance. This is particularly valuable in performance-critical applications like JSON serialization, logging, and data processing where many integer conversions occur.

Key concepts:

  1. Integer trait — provides optimized to_string() for integer types
  2. Buffer reuse — avoid allocations by writing into a pre-allocated buffer
  3. No panics — conversion never panics, always succeeds
  4. All integer types — supports i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
  5. Zero dependency — minimal crate with no dependencies

Code Example

# Cargo.toml
[dependencies]
itoa = "1"
use itoa::Integer;
 
fn main() {
    let number = 12345;
    let s = number.to_string(); // Uses itoa's fast implementation
    println!("Number as string: {}", s);
}

Basic Usage

use itoa::Integer;
 
fn main() {
    // Convert various integer types
    let num_i32: i32 = 42;
    let num_i64: i64 = -9876543210;
    let num_u64: u64 = 18446744073709551615;
    let num_u8: u8 = 255;
    let num_isize: isize = -100;
    
    println!("i32: {}", num_i32.to_string());
    println!("i64: {}", num_i64.to_string());
    println!("u64: {}", num_u64.to_string());
    println!("u8: {}", num_u8.to_string());
    println!("isize: {}", num_isize.to_string());
}

Using Buffer for Zero-Allocation Conversion

use itoa::{Buffer, Integer};
 
fn main() {
    // Create a reusable buffer
    let mut buffer = Buffer::new();
    
    // Write to buffer, returns a string slice
    let s1 = buffer.format(12345);
    println!("First: {}", s1);
    
    // Reuse the same buffer
    let s2 = buffer.format(-98765);
    println!("Second: {}", s2);
    
    // The buffer is large enough for any integer
    let s3 = buffer.format(i64::MIN);
    println!("i64::MIN: {}", s3);
    
    let s4 = buffer.format(u128::MAX);
    println!("u128::MAX: {}", s4);
}

Buffer Lifetime

use itoa::Buffer;
 
fn convert_to_string(n: i32) -> String {
    let mut buffer = Buffer::new();
    buffer.format(n).to_owned()
}
 
fn main() {
    // The buffer's string slice is only valid while buffer exists
    let result = convert_to_string(42);
    println!("Converted: {}", result);
    
    // For multiple conversions, keep buffer around
    let mut buffer = Buffer::new();
    let numbers = [1, 2, 3, 4, 5];
    
    for n in numbers {
        let s = buffer.format(n);
        println!("Number: {}", s);
        // s is only valid until next format() call or buffer is dropped
    }
}

Writing to String Directly

use itoa::{Buffer, Integer};
 
fn main() {
    let mut output = String::new();
    let mut buffer = Buffer::new();
    
    let numbers: Vec<i32> = (0..10).collect();
    
    for n in numbers {
        output.push_str(buffer.format(n));
        output.push_str(", ");
    }
    
    println!("Numbers: {}", output.trim_end_matches(", "));
}

Comparing Performance

use itoa::{Buffer, Integer};
use std::time::Instant;
 
fn bench_std_to_string(count: usize) -> std::time::Duration {
    let start = Instant::now();
    let mut total_len = 0;
    
    for i in 0..count {
        let n = (i as i64) % 1000000;
        let s = n.to_string(); // Standard library
        total_len += s.len();
    }
    
    start.elapsed()
}
 
fn bench_itoa_to_string(count: usize) -> std::time::Duration {
    use itoa::Integer;
    let start = Instant::now();
    let mut total_len = 0;
    
    for i in 0..count {
        let n = (i as i64) % 1000000;
        let s = n.to_string(); // itoa's Integer trait
        total_len += s.len();
    }
    
    start.elapsed()
}
 
fn bench_itoa_buffer(count: usize) -> std::time::Duration {
    let start = Instant::now();
    let mut buffer = Buffer::new();
    let mut total_len = 0;
    
    for i in 0..count {
        let n = (i as i64) % 1000000;
        let s = buffer.format(n);
        total_len += s.len();
    }
    
    start.elapsed()
}
 
fn main() {
    let count = 1_000_000;
    
    let std_time = bench_std_to_string(count);
    let itoa_time = bench_itoa_to_string(count);
    let buffer_time = bench_itoa_buffer(count);
    
    println!("Standard to_string: {:?}", std_time);
    println!("itoa to_string: {:?}", itoa_time);
    println!("itoa buffer: {:?}", buffer_time);
    println!("\nSpeedup vs std:");
    println!("  itoa to_string: {:.2}x", std_time.as_secs_f64() / itoa_time.as_secs_f64());
    println!("  itoa buffer: {:.2}x", std_time.as_secs_f64() / buffer_time.as_secs_f64());
}

Large Integers

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Maximum values for various types
    println!("u8::MAX = {}", buffer.format(u8::MAX));
    println!("u16::MAX = {}", buffer.format(u16::MAX));
    println!("u32::MAX = {}", buffer.format(u32::MAX));
    println!("u64::MAX = {}", buffer.format(u64::MAX));
    println!("u128::MAX = {}", buffer.format(u128::MAX));
    
    // Minimum values for signed types
    println!("i8::MIN = {}", buffer.format(i8::MIN));
    println!("i16::MIN = {}", buffer.format(i16::MIN));
    println!("i32::MIN = {}", buffer.format(i32::MIN));
    println!("i64::MIN = {}", buffer.format(i64::MIN));
    println!("i128::MIN = {}", buffer.format(i128::MIN));
}

Real-World: CSV Writer

use itoa::Buffer;
 
struct CsvWriter {
    buffer: Buffer,
    output: String,
}
 
impl CsvWriter {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
            output: String::new(),
        }
    }
    
    fn write_field(&mut self, value: i64) {
        self.output.push_str(self.buffer.format(value));
    }
    
    fn write_fields(&mut self, values: &[i64]) {
        for (i, value) in values.iter().enumerate() {
            if i > 0 {
                self.output.push(',');
            }
            self.write_field(*value);
        }
        self.output.push('\n');
    }
    
    fn write_row(&mut self, values: &[i64]) {
        self.write_fields(values);
    }
    
    fn get_output(&self) -> &str {
        &self.output
    }
    
    fn clear(&mut self) {
        self.output.clear();
    }
}
 
fn main() {
    let mut writer = CsvWriter::new();
    
    writer.write_row(&[1, 2, 3, 4, 5]);
    writer.write_row(&[10, 20, 30, 40, 50]);
    writer.write_row(&[100, 200, 300, 400, 500]);
    
    print!("{}", writer.get_output());
}

Real-World: JSON Number Serialization

use itoa::Buffer;
 
struct JsonWriter {
    buffer: Buffer,
    output: String,
}
 
impl JsonWriter {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
            output: String::new(),
        }
    }
    
    fn write_number(&mut self, n: i64) {
        self.output.push_str(self.buffer.format(n));
    }
    
    fn write_string(&mut self, s: &str) {
        self.output.push('"');
        self.output.push_str(s);
        self.output.push('"');
    }
    
    fn write_key_value(&mut self, key: &str, value: i64) {
        self.write_string(key);
        self.output.push(':');
        self.write_number(value);
    }
    
    fn write_object(&mut self, entries: &[(&str, i64)]) {
        self.output.push('{');
        for (i, (key, value)) in entries.iter().enumerate() {
            if i > 0 {
                self.output.push(',');
            }
            self.write_key_value(key, *value);
        }
        self.output.push('}');
    }
    
    fn get_output(&self) -> &str {
        &self.output
    }
}
 
fn main() {
    let mut writer = JsonWriter::new();
    
    writer.write_object(&[
        ("id", 12345),
        ("count", 100),
        ("price", 9999),
        ("quantity", 42),
    ]);
    
    println!("{}", writer.get_output());
}

Real-World: Log Formatter

use itoa::Buffer;
use std::time::{SystemTime, UNIX_EPOCH};
 
struct LogFormatter {
    buffer: Buffer,
}
 
impl LogFormatter {
    fn new() -> Self {
        Self { buffer: Buffer::new() }
    }
    
    fn format_timestamp(&mut self, ts: u64) -> &str {
        self.buffer.format(ts)
    }
    
    fn format_log(&mut self, level: &str, code: i32, message: &str) -> String {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        
        let mut output = String::new();
        output.push('[');
        output.push_str(self.buffer.format(timestamp));
        output.push_str("] [");
        output.push_str(level);
        output.push_str("] [");
        output.push_str(self.buffer.format(code));
        output.push_str("] ");
        output.push_str(message);
        
        output
    }
}
 
fn main() {
    let mut formatter = LogFormatter::new();
    
    println!("{}", formatter.format_log("INFO", 200, "Request processed"));
    println!("{}", formatter.format_log("ERROR", 500, "Internal error"));
    println!("{}", formatter.format_log("WARN", 429, "Rate limit exceeded"));
}

Real-World: Counter Display

use itoa::Buffer;
 
struct Counter {
    value: u64,
    buffer: Buffer,
}
 
impl Counter {
    fn new() -> Self {
        Self {
            value: 0,
            buffer: Buffer::new(),
        }
    }
    
    fn increment(&mut self) {
        self.value += 1;
    }
    
    fn increment_by(&mut self, delta: u64) {
        self.value += delta;
    }
    
    fn get(&self) -> u64 {
        self.value
    }
    
    fn display(&mut self) -> &str {
        self.buffer.format(self.value)
    }
}
 
fn main() {
    let mut counter = Counter::new();
    
    for _ in 0..10 {
        counter.increment();
        println!("Count: {}", counter.display());
    }
    
    counter.increment_by(90);
    println!("Final: {}", counter.display());
}

Real-World: HTTP Response Headers

use itoa::Buffer;
 
struct HttpResponseBuilder {
    buffer: Buffer,
    headers: String,
}
 
impl HttpResponseBuilder {
    fn new() -> Self {
        Self {
            buffer: Buffer::new(),
            headers: String::new(),
        }
    }
    
    fn status_line(&mut self, code: u16, message: &str) -> &mut Self {
        self.headers.push_str("HTTP/1.1 ");
        self.headers.push_str(self.buffer.format(code));
        self.headers.push(' ');
        self.headers.push_str(message);
        self.headers.push_str("\r\n");
        self
    }
    
    fn header(&mut self, name: &str, value: &str) -> &mut Self {
        self.headers.push_str(name);
        self.headers.push_str(": ");
        self.headers.push_str(value);
        self.headers.push_str("\r\n");
        self
    }
    
    fn content_length(&mut self, len: usize) -> &mut Self {
        self.headers.push_str("Content-Length: ");
        self.headers.push_str(self.buffer.format(len));
        self.headers.push_str("\r\n");
        self
    }
    
    fn build(&mut self) -> String {
        self.headers.push_str("\r\n");
        self.headers.clone()
    }
}
 
fn main() {
    let mut builder = HttpResponseBuilder::new();
    
    let response = builder
        .status_line(200, "OK")
        .header("Content-Type", "text/plain")
        .content_length(13)
        .header("Connection", "keep-alive")
        .build();
    
    println!("{}", response);
}

Real-World: Metric Collector

use itoa::Buffer;
use std::collections::HashMap;
 
struct Metrics {
    counters: HashMap<String, u64>,
    buffer: Buffer,
}
 
impl Metrics {
    fn new() -> Self {
        Self {
            counters: HashMap::new(),
            buffer: Buffer::new(),
        }
    }
    
    fn increment(&mut self, name: &str) {
        *self.counters.entry(name.to_string()).or_insert(0) += 1;
    }
    
    fn add(&mut self, name: &str, value: u64) {
        *self.counters.entry(name.to_string()).or_insert(0) += value;
    }
    
    fn export(&mut self) -> String {
        let mut output = String::new();
        
        for (name, value) in &self.counters {
            output.push_str(name);
            output.push(' ');
            output.push_str(self.buffer.format(*value));
            output.push('\n');
        }
        
        output
    }
}
 
fn main() {
    let mut metrics = Metrics::new();
    
    for _ in 0..100 {
        metrics.increment("requests");
    }
    
    metrics.add("bytes_sent", 1024 * 100);
    metrics.add("errors", 5);
    
    print!("{}", metrics.export());
}

Writing to io::Write

use itoa::Buffer;
use std::io::{self, Write};
 
fn write_integers<W: Write>(writer: &mut W, numbers: &[i32]) -> io::Result<()> {
    let mut buffer = Buffer::new();
    
    for n in numbers {
        writer.write_all(buffer.format(*n).as_bytes())?;
        writer.write_all(b"\n")?;
    }
    
    Ok(())
}
 
fn main() -> io::Result<()> {
    let numbers: Vec<i32> = (1..=10).collect();
    
    // Write to stdout
    let mut stdout = io::stdout();
    write_integers(&mut stdout, &numbers)?;
    
    // Write to file
    let mut file = std::fs::File::create("numbers.txt")?;
    write_integers(&mut file, &numbers)?;
    
    println!("\nWritten to numbers.txt");
    
    std::fs::remove_file("numbers.txt")?;
    Ok(())
}

Nested Structures

use itoa::Buffer;
 
struct TableFormatter {
    buffer: Buffer,
}
 
impl TableFormatter {
    fn new() -> Self {
        Self { buffer: Buffer::new() }
    }
    
    fn format_row(&mut self, cells: &[i64]) -> String {
        let mut row = String::new();
        
        for (i, cell) in cells.iter().enumerate() {
            if i > 0 {
                row.push_str(" | ");
            }
            let s = self.buffer.format(*cell);
            // Pad to width 10
            for _ in 0..(10 - s.len()) {
                row.push(' ');
            }
            row.push_str(s);
        }
        
        row
    }
    
    fn format_table(&mut self, data: &[Vec<i64>]) -> String {
        let mut table = String::new();
        
        for row in data {
            table.push_str(&self.format_row(row));
            table.push('\n');
        }
        
        table
    }
}
 
fn main() {
    let mut formatter = TableFormatter::new();
    
    let data = vec![
        vec![1, 100, 1000, 10000],
        vec![22, 2200, 22000, 220000],
        vec![333, 33300, 333000, 3330000],
    ];
    
    print!("{}", formatter.format_table(&data));
}

Integer Types

use itoa::{Buffer, Integer};
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Unsigned types
    let u8_val: u8 = 255;
    let u16_val: u16 = 65535;
    let u32_val: u32 = 4294967295;
    let u64_val: u64 = 18446744073709551615;
    let u128_val: u128 = 340282366920938463463374607431768211455;
    let usize_val: usize = 18446744073709551615;
    
    println!("Unsigned types:");
    println!("  u8: {}", buffer.format(u8_val));
    println!("  u16: {}", buffer.format(u16_val));
    println!("  u32: {}", buffer.format(u32_val));
    println!("  u64: {}", buffer.format(u64_val));
    println!("  u128: {}", buffer.format(u128_val));
    println!("  usize: {}", buffer.format(usize_val));
    
    // Signed types
    let i8_val: i8 = -128;
    let i16_val: i16 = -32768;
    let i32_val: i32 = -2147483648;
    let i64_val: i64 = -9223372036854775808;
    let i128_val: i128 = -170141183460469231731687303715884105728;
    let isize_val: isize = -9223372036854775808;
    
    println!("\nSigned types (minimums):");
    println!("  i8: {}", buffer.format(i8_val));
    println!("  i16: {}", buffer.format(i16_val));
    println!("  i32: {}", buffer.format(i32_val));
    println!("  i64: {}", buffer.format(i64_val));
    println!("  i128: {}", buffer.format(i128_val));
    println!("  isize: {}", buffer.format(isize_val));
    
    // Maximum signed values
    println!("\nSigned types (maximums):");
    println!("  i8: {}", buffer.format(i8::MAX));
    println!("  i16: {}", buffer.format(i16::MAX));
    println!("  i32: {}", buffer.format(i32::MAX));
    println!("  i64: {}", buffer.format(i64::MAX));
}

Edge Cases

use itoa::Buffer;
 
fn main() {
    let mut buffer = Buffer::new();
    
    // Zero
    println!("Zero: '{}'", buffer.format(0));
    
    // Single digit
    println!("Single digit: '{}'", buffer.format(7));
    
    // Negative single digit
    println!("Negative single: '{}'", buffer.format(-3));
    
    // Powers of 10
    println!("10^0: '{}'", buffer.format(1i64));
    println!("10^1: '{}'", buffer.format(10i64));
    println!("10^2: '{}'", buffer.format(100i64));
    println!("10^3: '{}'", buffer.format(1000i64));
    println!("10^6: '{}'", buffer.format(1_000_000i64));
    println!("10^9: '{}'", buffer.format(1_000_000_000i64));
    
    // Boundary values
    println!("i32::MAX: '{}'", buffer.format(i32::MAX));
    println!("i32::MIN: '{}'", buffer.format(i32::MIN));
    println!("i64::MAX: '{}'", buffer.format(i64::MAX));
    println!("i64::MIN: '{}'", buffer.format(i64::MIN));
}

Thread Safety

use itoa::Buffer;
use std::sync::{Arc, Mutex};
use std::thread;
 
fn main() {
    // Each thread should have its own buffer
    let handles: Vec<_> = (0..4)
        .map(|t| {
            thread::spawn(move || {
                let mut buffer = Buffer::new();
                let mut output = String::new();
                
                for i in 0..100 {
                    let n = t * 1000 + i;
                    output.push_str(buffer.format(n));
                    output.push(',');
                }
                
                output
            })
        })
        .collect();
    
    for handle in handles {
        let result = handle.join().unwrap();
        println!("Thread output (first 50 chars): {}", &result[..50.min(result.len())]);
    }
}

Summary

  • itoa::Integer trait provides fast to_string() for integers
  • Buffer::new() creates a reusable conversion buffer
  • buffer.format(n) converts integer to string slice (no allocation)
  • Supports all integer types: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
  • Significantly faster than standard library for integer-to-string conversion
  • Zero dependencies, minimal overhead
  • Perfect for JSON serialization, logging, metrics, CSV writing
  • Reuse Buffer for best performance in hot paths
  • Thread-safe when each thread has its own buffer
  • Handles all edge cases: zero, min/max values, negative numbers