Why can deriving Copy pessimize performance by 60%?
Until now, my understanding was that Copy is just a marker trait for the compiler to allow use after move. Therefore, it influences what code is accepted as correct, but should not influence code generation. In particular, I thought it's always a good idea to derive Copy when possible.
So imagine my surprise when I experimented with the following type:
#[derive(Clone)]
enum IntOrBool {
Int(i64),
Bool(bool),
}
On my machine (AMD Ryzen 5 3600), cloning a value of this type takes \2.5ns. However, when I additionally derive Copy, that time goes up to \4ns. That's 60% slower!
Does anyone know what's going on here? The only explanation I can possibly think of is that the compiler does some ostensible optimizations only allowed due to Copy semantics, which unfortunately end up being pessimizations after all? (Indeed, when I disable optimizations, the effect goes away.) But that sounds rather odd and hand-wavy, so I would really love to understand that in detail.
PS Some anticipatory clarifications:
- If the type implements Copy, why call .clone() instead of just copying? I think both should be compiled to exactly the same thing, so it shouldn't matter. But just to make sure, I also measured it, and yep, I get the same slowed down performance when copying directly.
- Maybe these differences in measurement are a weird side effect of the artificial microbenchmark setup? Certainly possible. Also, I'm definitely not a pro with these types of benchmarks, so maybe I did something stupid and these values are not really measuring what I think they do. However, the reason I started looking into this and found this minimal example is that I came across this effect in a real-world setting: An language interpreter I wrote slowed down by about 10% end-to-end when I added Copy to a type where I initially forgot it. So there is definitely a real effect here.
EDIT: Here is my benchmarking code (uses criterion):
use criterion::{criterion_group, criterion_main, Criterion};
use std::hint::black_box;
fn criterion_benchmark(c: &mut Criterion) {
let copyable = Copyable::Bool(false);
let not_copyable = NotCopyable::Bool(false);
let mut group = c.benchmark_group("copy slowdown");
group.bench_function("copy", |b| b.iter(|| black_box(©able).clone()));
group.bench_function("no copy", |b| b.iter(|| black_box(¬_copyable).clone()));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
#[derive(Clone, Copy)]
pub enum Copyable {
Int(i64),
Bool(bool),
}
#[derive(Clone)]
pub enum NotCopyable {
Int(i64),
Bool(bool),
}