How do I convert integers to strings fast with itoa in Rust?
Walkthrough
The itoa crate provides ultra-fast integer-to-string conversion. While Rust's standard library ToString::to_string() works fine for most cases, itoa can be 2-10x faster by avoiding heap allocation in many cases and using highly optimized conversion algorithms. It's particularly valuable in performance-critical code like serialization, logging, and data processing where millions of integer conversions occur.
Key concepts:
- itoa::fmt — writes formatted integers directly to a
fmt::Writetarget - itoa::Buffer — a reusable buffer that avoids repeated allocations
- No heap allocation — the buffer lives on the stack
- Write trait — integrates with
std::fmt::Writeandstd::io::Write - All integer types — supports i8, i16, i32, i64, i128, isize, and unsigned variants
Code Example
# Cargo.toml
[dependencies]
itoa = "1.0"use itoa::Buffer;
fn main() {
let mut buffer = Buffer::new();
let value: i32 = 42;
let s = buffer.format(value);
println!("{}", s); // "42"
let large: i64 = 1234567890123456789;
let s = buffer.format(large);
println!("{}", s); // "1234567890123456789"
let negative: i32 = -98765;
let s = buffer.format(negative);
println!("{}", s); // "-98765"
}Basic Usage
use itoa::Buffer;
fn main() {
let mut buffer = Buffer::new();
// Format various integer types
let a: i8 = 127;
println!("i8: {}", buffer.format(a));
let b: u8 = 255;
println!("u8: {}", buffer.format(b));
let c: i16 = -32768;
println!("i16: {}", buffer.format(c));
let d: u16 = 65535;
println!("u16: {}", buffer.format(d));
let e: i32 = -2147483648;
println!("i32: {}", buffer.format(e));
let f: u32 = 4294967295;
println!("u32: {}", buffer.format(f));
let g: i64 = -9223372036854775808;
println!("i64: {}", buffer.format(g));
let h: u64 = 18446744073709551615;
println!("u64: {}", buffer.format(h));
let i: i128 = -170141183460469231731687303715884105728;
println!("i128: {}", buffer.format(i));
let j: u128 = 340282366920938463463374607431768211455;
println!("u128: {}", buffer.format(j));
let k: isize = -42;
println!("isize: {}", buffer.format(k));
let l: usize = 42;
println!("usize: {}", buffer.format(l));
}Buffer Reuse
use itoa::Buffer;
fn main() {
// Create buffer once, reuse many times
let mut buffer = Buffer::new();
// Each call to format() reuses the same buffer
for i in 0..10 {
let s = buffer.format(i);
println!("Iteration {}: {}", i, s);
}
// The buffer is overwritten each time
// Previous results are no longer valid
let s1 = buffer.format(100);
println!("s1: {}", s1); // "100"
let s2 = buffer.format(200);
println!("s2: {}", s2); // "200"
// s1 may now be invalid - don't use it after reusing buffer
}Comparing with Standard Library
use itoa::Buffer;
fn main() {
let value: i32 = 1234567890;
// Standard library approach
let s1 = value.to_string();
println!("std::to_string: {}", s1);
// itoa approach
let mut buffer = Buffer::new();
let s2 = buffer.format(value);
println!("itoa::format: {}", s2);
// Both produce the same result
assert_eq!(s1, s2);
// But itoa avoids heap allocation
// to_string() allocates a new String each time
// Buffer::format() returns a &str into stack memory
}Performance Benchmark
use itoa::Buffer;
use std::time::Instant;
fn main() {
const ITERATIONS: usize = 10_000_000;
let numbers: Vec<i32> = (0..ITERATIONS as i32).collect();
// Benchmark std::to_string()
let start = Instant::now();
let mut total_len = 0;
for n in &numbers {
let s = n.to_string();
total_len += s.len();
}
let std_duration = start.elapsed();
println!("std::to_string(): {:?} (total_len = {})", std_duration, total_len);
// Benchmark itoa::Buffer::format()
let start = Instant::now();
let mut buffer = Buffer::new();
let mut total_len = 0;
for n in &numbers {
let s = buffer.format(*n);
total_len += s.len();
}
let itoa_duration = start.elapsed();
println!("itoa::format(): {:?} (total_len = {})", itoa_duration, total_len);
let speedup = std_duration.as_nanos() as f64 / itoa_duration.as_nanos() as f64;
println!("Speedup: {:.2}x", speedup);
}Writing to fmt::Write
use itoa;
use std::fmt::{self, Write};
struct NumberList {
numbers: Vec<i32>,
}
impl fmt::Display for NumberList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for n in &self.numbers {
if !first {
f.write_str(", ")?;
}
first = false;
// Use itoa to write directly without allocation
itoa::fmt(f, *n)?;
}
Ok(())
}
}
fn main() {
let list = NumberList {
numbers: vec![1, 2, 3, 4, 5, 100, -42, 999999],
};
println!("Numbers: {}", list);
// Build string manually
let mut output = String::new();
for i in 0..10 {
if i > 0 {
output.push_str(", ");
}
itoa::fmt(&mut output, i * i).unwrap();
}
println!("Squares: {}", output);
}Writing to io::Write
use itoa::Buffer;
use std::io::{self, Write};
fn main() -> io::Result<()> {
let mut stdout = io::stdout().lock();
let mut buffer = Buffer::new();
// Write integers directly to stdout
for i in 0..5 {
let s = buffer.format(i);
stdout.write_all(s.as_bytes())?;
stdout.write_all(b"\n")?;
}
// Write to a Vec<u8>
let mut output = Vec::new();
for i in 100..105 {
let s = buffer.format(i);
output.extend_from_slice(s.as_bytes());
output.push(b' ');
}
let result = String::from_utf8(output).unwrap();
println!("Output: {}", result);
Ok(())
}Building CSV Output
use itoa::Buffer;
fn main() {
let data = vec![
(1, "Alice", 95),
(2, "Bob", 87),
(3, "Charlie", 92),
(4, "Diana", 88),
];
let mut buffer = Buffer::new();
let mut csv = String::new();
// Header
csv.push_str("id,name,score\n");
// Data rows - use itoa for integer fields
for (id, name, score) in &data {
csv.push_str(buffer.format(*id));
csv.push(',');
csv.push_str(name);
csv.push(',');
csv.push_str(buffer.format(*score));
csv.push('\n');
}
print!("{}", csv);
}JSON-like Output
use itoa::Buffer;
fn main() {
let numbers: Vec<i32> = (0..10).collect();
let mut buffer = Buffer::new();
let mut json = String::new();
json.push('[');
for (i, n) in numbers.iter().enumerate() {
if i > 0 {
json.push(',');
}
json.push_str(buffer.format(*n));
}
json.push(']');
println!("{}", json);
}Custom Number Formatting
use itoa::Buffer;
use std::fmt::Write;
fn format_with_commas(n: i64) -> String {
let mut buffer = Buffer::new();
let num_str = buffer.format(n.abs());
let mut result = String::new();
if n < 0 {
result.push('-');
}
// Add commas every 3 digits from right
let chars: Vec<char> = num_str.chars().collect();
for (i, c) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i) % 3 == 0 {
result.push(',');
}
result.push(*c);
}
result
}
fn main() {
println!("{}", format_with_commas(1234567890));
println!("{}", format_with_commas(42));
println!("{}", format_with_commas(-9876543210));
println!("{}", format_with_commas(1000000));
}Memory-Efficient Counter Display
use itoa::Buffer;
use std::io::{self, Write};
struct ProgressDisplay {
current: u64,
total: u64,
buffer: Buffer,
}
impl ProgressDisplay {
fn new(total: u64) -> Self {
Self {
current: 0,
total,
buffer: Buffer::new(),
}
}
fn update(&mut self, current: u64) {
self.current = current;
}
fn display(&self) {
let current_str = self.buffer.format(self.current);
let total_str = self.buffer.format(self.total);
// Note: This is just for demonstration - in real code,
// we'd need to handle the borrowing issue differently
eprint!("\rProgress: {}/{} ({:.1}%)",
current_str,
self.total,
(self.current as f64 / self.total as f64) * 100.0
);
}
}
fn main() {
let mut progress = ProgressDisplay::new(100);
for i in 0..=100 {
progress.update(i);
progress.display();
std::thread::sleep(std::time::Duration::from_millis(50));
}
println!();
}Handling Large Numbers
use itoa::Buffer;
fn main() {
let mut buffer = Buffer::new();
// u64 max
let max_u64: u64 = u64::MAX;
println!("u64::MAX = {}", buffer.format(max_u64));
// i64 min and max
let min_i64: i64 = i64::MIN;
println!("i64::MIN = {}", buffer.format(min_i64));
let max_i64: i64 = i64::MAX;
println!("i64::MAX = {}", buffer.format(max_i64));
// i128 values
let big: i128 = 123456789012345678901234567890;
println!("i128 = {}", buffer.format(big));
// usize for sizes
let size: usize = 1024 * 1024 * 1024; // 1 GB in bytes
println!("Size: {} bytes", buffer.format(size));
}Integrating with Serde
use itoa::Buffer;
use std::fmt;
// Custom display wrapper for fast integer formatting
struct FastInt<T>(T);
impl<T: itoa::Integer> fmt::Display for FastInt<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer = Buffer::new();
f.write_str(buffer.format(self.0))
}
}
fn main() {
let value: i32 = 12345;
println!("Fast: {}", FastInt(value));
// When writing custom serialization
let mut buffer = Buffer::new();
// Build JSON array of integers
let mut json = String::new();
json.push('[');
for i in -5..=5 {
if i > -5 {
json.push(',');
}
json.push_str(buffer.format(i));
}
json.push(']');
println!("JSON: {}", json);
}Real-World Example: Log Formatter
use itoa::Buffer;
use std::io::{self, Write};
use std::time::{SystemTime, UNIX_EPOCH};
struct LogEntry {
timestamp: u64,
level: &'static str,
message: &'static str,
code: i32,
}
impl LogEntry {
fn format_into(&self, buffer: &mut Buffer, output: &mut Vec<u8>) {
// Timestamp
output.extend_from_slice(b"[");
output.extend_from_slice(buffer.format(self.timestamp).as_bytes());
output.extend_from_slice(b"] ");
// Level
output.extend_from_slice(self.level.as_bytes());
output.extend_from_slice(b": ");
// Message
output.extend_from_slice(self.message.as_bytes());
// Code
output.extend_from_slice(b" (code: ");
output.extend_from_slice(buffer.format(self.code).as_bytes());
output.extend_from_slice(b")\n");
}
}
fn main() {
let entries = vec![
LogEntry { timestamp: 1705312800, level: "INFO", message: "Server started", code: 0 },
LogEntry { timestamp: 1705312805, level: "WARN", message: "High memory usage", code: 1001 },
LogEntry { timestamp: 1705312810, level: "ERROR", message: "Connection failed", code: 5003 },
LogEntry { timestamp: 1705312815, level: "INFO", message: "Reconnected", code: 0 },
];
let mut buffer = Buffer::new();
let mut output = Vec::new();
for entry in &entries {
entry.format_into(&mut buffer, &mut output);
}
// Write to stdout
io::stdout().write_all(&output).unwrap();
}Real-World Example: Metrics Collector
use itoa::Buffer;
use std::collections::HashMap;
struct Metrics {
counters: HashMap<&'static str, u64>,
gauges: HashMap<&'static str, i64>,
}
impl Metrics {
fn new() -> Self {
Self {
counters: HashMap::new(),
gauges: HashMap::new(),
}
}
fn increment(&mut self, name: &'static str, delta: u64) {
*self.counters.entry(name).or_insert(0) += delta;
}
fn set_gauge(&mut self, name: &'static str, value: i64) {
self.gauges.insert(name, value);
}
fn format_prometheus(&self) -> String {
let mut buffer = Buffer::new();
let mut output = String::new();
for (name, value) in &self.counters {
output.push_str(name);
output.push(' ');
output.push_str(buffer.format(*value));
output.push('\n');
}
for (name, value) in &self.gauges {
output.push_str(name);
output.push(' ');
output.push_str(buffer.format(*value));
output.push('\n');
}
output
}
}
fn main() {
let mut metrics = Metrics::new();
metrics.increment("requests_total", 1);
metrics.increment("requests_total", 1);
metrics.increment("errors_total", 1);
metrics.set_gauge("active_connections", 42);
metrics.set_gauge("memory_bytes", 1024000);
println!("{}", metrics.format_prometheus());
}Real-World Example: Binary Protocol Encoder
use itoa::Buffer;
use std::io::{self, Write};
struct PacketEncoder {
buffer: Buffer,
output: Vec<u8>,
}
impl PacketEncoder {
fn new() -> Self {
Self {
buffer: Buffer::new(),
output: Vec::new(),
}
}
fn write_header(&mut self, packet_type: u8, length: usize) {
self.output.push(packet_type);
// Length-prefixed as ASCII digits with null terminator
let len_str = self.buffer.format(length);
self.output.extend_from_slice(len_str.as_bytes());
self.output.push(0); // null terminator
}
fn write_int_field(&mut self, value: i64) {
let s = self.buffer.format(value);
self.output.extend_from_slice(s.as_bytes());
self.output.push(b'|'); // field separator
}
fn write_string_field(&mut self, value: &str) {
self.output.extend_from_slice(value.as_bytes());
self.output.push(b'|');
}
fn finish(self) -> Vec<u8> {
self.output
}
}
fn main() {
let mut encoder = PacketEncoder::new();
// Encode a simple packet
encoder.write_header(1, 3); // Type 1, 3 fields
encoder.write_int_field(12345);
encoder.write_int_field(-9876);
encoder.write_string_field("hello");
let packet = encoder.finish();
println!("Packet ({} bytes): {:?}", packet.len(), packet);
println!("As ASCII: {}", String::from_utf8_lossy(&packet));
}Thread-Safe Usage
use itoa::Buffer;
use std::sync::Mutex;
use std::thread;
fn main() {
// Each thread should have its own buffer
let handles: Vec<_> = (0..4)
.map(|thread_id| {
thread::spawn(move || {
let mut buffer = Buffer::new();
let mut results = Vec::new();
for i in 0..1000 {
let n = thread_id * 1000 + i;
let s = buffer.format(n);
results.push(s.to_string()); // Convert to owned if needed
}
results.len()
})
})
.collect();
let total: usize = handles.into_iter().map(|h| h.join().unwrap()).sum();
println!("Processed {} numbers", total);
}Comparison Table
use itoa::Buffer;
fn main() {
let value: i64 = 9876543210;
println!("Comparison for value {}:", value);
println!();
// Method 1: to_string() - allocates String
let s1 = value.to_string();
println!("to_string(): \"{}\" (allocates String)", s1);
// Method 2: format! macro - allocates String
let s2 = format!("{}", value);
println!("format!(\"{{}}\"): \"{}\" (allocates String)", s2);
// Method 3: itoa::Buffer - no allocation
let mut buffer = Buffer::new();
let s3 = buffer.format(value);
println!("itoa::Buffer: \"{}\" (stack buffer, no allocation)", s3);
// Method 4: write! to String - allocates
let mut s4 = String::new();
std::fmt::write(&mut s4, format_args!("{}", value)).unwrap();
println!("write! to String: \"{}\" (allocates String)", s4);
// Method 5: itoa::fmt to String - no intermediate allocation
let mut s5 = String::new();
itoa::fmt(&mut s5, value).unwrap();
println!("itoa::fmt: \"{}\" (writes directly)", s5);
}Summary
itoaprovides fast integer-to-string conversion without heap allocation- Create a
Bufferonce and reuse it withformat()for multiple conversions format()returns a&strthat borrows from the buffer — it's overwritten on next callitoa::fmt()writes directly to anyfmt::Writeorio::Writeimplementation- Supports all integer types: i8, i16, i32, i64, i128, isize, and unsigned variants
- 2-10x faster than
to_string()for repeated conversions - Use in performance-critical paths: serialization, logging, metrics, network protocols
- Each thread should have its own buffer (Buffer is not thread-safe)
- No dependencies, minimal API, highly optimized assembly for common platforms
