Why can deriving Copy pessimize performance by 60%?

75 points jogru0

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(&copyable).clone()));
    group.bench_function("no copy", |b| b.iter(|| black_box(&not_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),
}