Just of curiosity, which CPU you using to running?
I am curious, why you have so different results. Maybe you running something in parallel?
Like mining bitcoins? 
You dynamic results are steady, but static is not.
how slow you can get dynamic dispatch to be?
Yes, exactly.
Imagine you have:
struct MyStruct {
val: i64,
}
impl MyStruct {
fn increment(&mut self) {
self.val += 1;
}
}
And in main:
fn main() {
let mut my_struct = MyStruct { val: 5 };
my_struct.increment();
println!("{}", my_struct.val);
}
What the compiler can essentially do is inline the method:
fn main() {
let mut my_struct = MyStruct { val: 5 };
my_struct.val += 1;
println!("{}", my_struct.val);
}
Then optimize the struct away:
fn main() {
let mut my_struct_val = 5;
my_struct_val += 1;
println!("{}", my_struct_val);
}
Then constant-propagate the value:
fn main() {
let my_struct_val = 6;
println!("{}", my_struct_val);
}
Until the relevant part of the code effectively becomes:
fn main() {
println!("{}", 6);
}
And eventually:
fn main() {
println!("6");
}
Rust/LLVM is pretty smart here.
If you do the same thing with traits:
trait Increment {
fn increment(&mut self);
}
impl Increment for MyStruct {
fn increment(&mut self) {
self.val += 1;
}
}
and use the trait statically, for example through a generic function, you can get essentially the same optimization because the concrete type is known after monomorphization.
But there are situations where your design forces you to use dyn Trait, and the compiler cannot devirtualize the call because the concrete implementation is deliberately hidden:
fn main() {
let mut my_struct = MyStruct { val: 5 };
let incr_trait: &mut dyn Increment =
std::hint::black_box(&mut my_struct);
incr_trait.increment();
println!("{}", my_struct.val);
}
This line:
let incr_trait: &mut dyn Increment =
std::hint::black_box(&mut my_struct);
conceptually gives us something like:
incr_trait = {
data: pointer to my_struct,
vtable: pointer to MyStruct's Increment vtable,
}
On x86-64, you can think about those two pointers as something like:
lea rdi, [my_struct] ; data pointer
lea rsi, [Increment_vtable] ; vtable pointer
Each lea shown here is a real CPU instruction: if these instructions are present in the final machine code, the CPU has to execute them, and that takes CPU resources and execution time.
Then, when you do:
incr_trait.increment();
the dynamic call is conceptually something like:
// Get the two parts of the trait object
let data_ptr = incr_trait.data;
// rdi = data_ptr
let vtable_ptr = incr_trait.vtable;
// rsi = vtable_ptr
// Load the address of Increment::increment from the vtable
let increment_fn = vtable_ptr.increment;
// mov rax, [rsi + OFFSET]
// Indirect call, passing data_ptr as `self`
increment_fn(data_ptr);
// call rax
The CPU may also perform the function-pointer load and indirect call as essentially one instruction:
call qword ptr [rsi + OFFSET]
You can see that there is more happening at runtime:
- We have to get the address of the function from the vtable, which requires a memory read. Ideally it comes from CPU cache; in the worst case it may have to be fetched from RAM.
- Then we have to make an indirect call to that address. The CPU has to execute that call, and it can become more expensive if the CPU predicts the destination incorrectly.
- And only then does the real work happen: incrementing the value in the struct.
So with a normal call, the compiler may be able to make the call disappear completely.
With dynamic dispatch, the CPU may actually have to find the function through the vtable, call it, execute it, and return from it.
If the compiler can prove what is being called and inline it, it really can make the application as simple as Hello World:
println!("6");
in this example.