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:
- Integer trait — provides optimized
to_string()for integer types - Buffer reuse — avoid allocations by writing into a pre-allocated buffer
- No panics — conversion never panics, always succeeds
- All integer types — supports i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
- 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::Integertrait provides fastto_string()for integersBuffer::new()creates a reusable conversion bufferbuffer.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
Bufferfor best performance in hot paths - Thread-safe when each thread has its own buffer
- Handles all edge cases: zero, min/max values, negative numbers
