In Java, there's a JIT that optimizes this at runtime. In Rust, there's no JIT, so Box<dyn> remains a vtable. This is slow because vtables cause cache misses, as the CPU can't predict them accurately
For example
use std::time::Instant;
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn sum_area_dynamic(shapes: &[Box<dyn Shape>]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn sum_area_static<T: Shape>(shapes: &[T]) -> f64 {
shapes.iter().map(|s| s.area()).sum()
}
fn benchmark_dynamic(n: usize, iterations: u32) -> std::time::Duration {
let shapes: Vec<Box<dyn Shape>> = (0..n)
.map(|i| -> Box<dyn Shape> {
if i % 2 == 0 {
Box::new(Circle { radius: i as f64 + 1.0 })
} else {
Box::new(Rectangle { width: i as f64 + 1.0, height: i as f64 + 2.0 })
}
})
.collect();
let start = Instant::now();
let mut total = 0.0f64;
for _ in 0..iterations {
total += sum_area_dynamic(&shapes);
}
let elapsed = start.elapsed();
std::hint::black_box(total);
elapsed
}
fn benchmark_static_circle(n: usize, iterations: u32) -> std::time::Duration {
let shapes: Vec<Circle> = (0..n)
.map(|i| Circle { radius: i as f64 + 1.0 })
.collect();
let start = Instant::now();
let mut total = 0.0f64;
for _ in 0..iterations {
total += sum_area_static(&shapes);
}
let elapsed = start.elapsed();
std::hint::black_box(total);
elapsed
}
fn benchmark_static_rectangle(n: usize, iterations: u32) -> std::time::Duration {
let shapes: Vec<Rectangle> = (0..n)
.map(|i| Rectangle { width: i as f64 + 1.0, height: i as f64 + 2.0 })
.collect();
let start = Instant::now();
let mut total = 0.0f64;
for _ in 0..iterations {
total += sum_area_static(&shapes);
}
let elapsed = start.elapsed();
std::hint::black_box(total);
elapsed
}
fn print_result(label: &str, elapsed: std::time::Duration, iterations: u32, n: usize) {
let total_ops = iterations as u64 * n as u64;
let ns_per_op = elapsed.as_nanos() as f64 / total_ops as f64;
println!(
"{:<40} | {:>10.3} ms | {:>8.3} ns/op",
label,
elapsed.as_secs_f64() * 1000.0,
ns_per_op
);
}
fn main() {
let n = 10_000;
let iterations = 1_000;
println!("Benchmark: vtable (Box<dyn>) vs non-vtable (generics/monomorphized)");
println!("Elements: {n}, Iterations: {iterations}");
println!("{}", "-".repeat(70));
println!("{:<40} | {:>10} | {:>12}", "Method", "Total Time", "ns/op");
println!("{}", "-".repeat(70));
let d = benchmark_dynamic(n, iterations);
print_result("Dynamic dispatch (Box<dyn Shape>)", d, iterations, n);
let sc = benchmark_static_circle(n, iterations);
print_result("Static dispatch (Circle only)", sc, iterations, n);
let sr = benchmark_static_rectangle(n, iterations);
print_result("Static dispatch (Rectangle only)", sr, iterations, n);
println!("{}", "-".repeat(70));
let dyn_ns = d.as_nanos() as f64;
let static_avg_ns = (sc.as_nanos() + sr.as_nanos()) as f64 / 2.0;
let overhead = ((dyn_ns - static_avg_ns) / static_avg_ns) * 100.0;
println!(
"\nVtable overhead vs static avg: {:.2}%",
overhead
);
}
The result :
Benchmark: vtable (Box<dyn>) vs non vtable (generics/monomorphized)
Elements: 10000, Iterations: 1000
----------------------------------------------------------------------
Method | Total Time | ns/op
----------------------------------------------------------------------
Dynamic dispatch (Box<dyn Shape>) | 34.441 ms | 3.444 ns/op
Static dispatch (Circle only) | 9.292 ms | 0.929 ns/op
Static dispatch (Rectangle only) | 9.205 ms | 0.921 ns/op
----------------------------------------------------------------------
Vtable overhead vs static avg: 272.39%
For Box<dyn> as error type :
use std::fmt;
use std::time::Instant;
#[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(String),
IoError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "NotFound: {}", msg),
AppError::ParseError(msg) => write!(f, "ParseError: {}", msg),
AppError::IoError(msg) => write!(f, "IoError: {}", msg),
}
}
}
impl std::error::Error for AppError {}
#[derive(Debug)]
struct NotFoundError(String);
#[derive(Debug)]
struct ParseError(String);
#[derive(Debug)]
struct IoError(String);
impl fmt::Display for NotFoundError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NotFound: {}", self.0)
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ParseError: {}", self.0)
}
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IoError: {}", self.0)
}
}
impl std::error::Error for NotFoundError {}
impl std::error::Error for ParseError {}
impl std::error::Error for IoError {}
fn process_dynamic(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
match input % 3 {
0 => Err(Box::new(NotFoundError(format!("item {input}")))),
1 => Err(Box::new(ParseError(format!("value {input}")))),
_ => Ok(input * 2),
}
}
fn process_static(input: i32) -> Result<i32, AppError> {
match input % 3 {
0 => Err(AppError::NotFound(format!("item {input}"))),
1 => Err(AppError::ParseError(format!("value {input}"))),
_ => Ok(input * 2),
}
}
fn bench_dynamic_mixed(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).collect();
let start = Instant::now();
let mut ok_count = 0usize;
let mut err_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
match process_dynamic(v) {
Ok(_) => ok_count += 1,
Err(_) => err_count += 1,
}
}
}
let elapsed = start.elapsed();
std::hint::black_box((ok_count, err_count));
elapsed
}
fn bench_static_mixed(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).collect();
let start = Instant::now();
let mut ok_count = 0usize;
let mut err_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
match process_static(v) {
Ok(_) => ok_count += 1,
Err(_) => err_count += 1,
}
}
}
let elapsed = start.elapsed();
std::hint::black_box((ok_count, err_count));
elapsed
}
fn bench_dynamic_all_err(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
let start = Instant::now();
let mut err_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
if process_dynamic(v).is_err() {
err_count += 1;
}
}
}
let elapsed = start.elapsed();
std::hint::black_box(err_count);
elapsed
}
fn bench_static_all_err(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
let start = Instant::now();
let mut err_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
if process_static(v).is_err() {
err_count += 1;
}
}
}
let elapsed = start.elapsed();
std::hint::black_box(err_count);
elapsed
}
fn bench_dynamic_no_err(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
let start = Instant::now();
let mut ok_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
if let Ok(x) = process_dynamic(v) {
ok_count += 1;
std::hint::black_box(x);
}
}
}
let elapsed = start.elapsed();
std::hint::black_box(ok_count);
elapsed
}
fn bench_static_no_err(n: usize, iterations: u32) -> std::time::Duration {
let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
let start = Instant::now();
let mut ok_count = 0usize;
for _ in 0..iterations {
for &v in &inputs {
if let Ok(x) = process_static(v) {
ok_count += 1;
std::hint::black_box(x);
}
}
}
let elapsed = start.elapsed();
std::hint::black_box(ok_count);
elapsed
}
fn print_result(label: &str, elapsed: std::time::Duration, iterations: u32, n: usize) {
let total_ops = iterations as u64 * n as u64;
let ns_per_op = elapsed.as_nanos() as f64 / total_ops as f64;
println!(
"{:<50} | {:>10.3} ms | {:>8.3} ns/op",
label,
elapsed.as_secs_f64() * 1000.0,
ns_per_op
);
}
fn print_overhead(label: &str, dyn_d: std::time::Duration, stat_d: std::time::Duration) {
let overhead = ((dyn_d.as_nanos() as f64 - stat_d.as_nanos() as f64)
/ stat_d.as_nanos() as f64)
* 100.0;
println!(" {label}: {overhead:+.2}%");
}
fn main() {
let n = 10_000;
let iterations = 1_000;
println!("Benchmark: Error Type โ Box<dyn Error> (vtable) vs Enum (non-vtable)");
println!("Elements : {n}, Iterations: {iterations}");
println!("{}", "=".repeat(80));
println!("\n[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)");
println!("{}", "-".repeat(80));
let d_mix = bench_dynamic_mixed(n, iterations);
print_result("Box<dyn Error> โ mixed ok/err", d_mix, iterations, n);
let s_mix = bench_static_mixed(n, iterations);
print_result("Enum AppError โ mixed ok/err", s_mix, iterations, n);
println!("\n[Skenario 2] All Errors (100% Err path โ heap alloc setiap iterasi)");
println!("{}", "-".repeat(80));
let d_err = bench_dynamic_all_err(n, iterations);
print_result("Box<dyn Error> โ all errors", d_err, iterations, n);
let s_err = bench_static_all_err(n, iterations);
print_result("Enum AppError โ all errors", s_err, iterations, n);
println!("\n[Skenario 3] All Ok (100% happy path โ error type tidak pernah dibuat)");
println!("{}", "-".repeat(80));
let d_ok = bench_dynamic_no_err(n, iterations);
print_result("Box<dyn Error> โ all ok", d_ok, iterations, n);
let s_ok = bench_static_no_err(n, iterations);
print_result("Enum AppError โ all ok", s_ok, iterations, n);
println!("\n{}", "=".repeat(80));
println!("Vtable overhead Box<dyn Error> vs Enum:");
print_overhead("Mixed ok/err ", d_mix, s_mix);
print_overhead("All errors ", d_err, s_err);
print_overhead("All ok ", d_ok, s_ok);
println!("{}", "=".repeat(80));
}
The result :
Benchmark: Error Type โ Box<dyn Error> (vtable) vs Enum (non vtable)
Elements : 10000, Iterations: 1000
================================================================================
[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error> โ mixed ok/err | 562.580 ms | 56.258 ns/op
Enum AppError โ mixed ok/err | 363.631 ms | 36.363 ns/op
[Skenario 2] All Errors (100% Err path โ heap alloc in each iteration)
--------------------------------------------------------------------------------
Box<dyn Error> โ all errors | 782.548 ms | 78.255 ns/op
Enum AppError โ all errors | 519.097 ms | 51.910 ns/op
[Skenario 3] All Ok (100% happy path โ error type is never created)
--------------------------------------------------------------------------------
Box<dyn Error> โ all ok | 35.413 ms | 3.541 ns/op
Enum AppError โ all ok | 14.964 ms | 1.496 ns/op
================================================================================
Vtable overhead Box<dyn Error> vs Enum:
Mixed ok/err : +54.71%
All errors : +50.75%
All ok : +136.66%