Getters for Copy types

We have a small discussion in our team how we should design our API around smallish non-trivial copy fields.

So lets say we have something like this:

#[derive(Copy)]
pub struct SmallishType {
     x: Duration,
     y: u32
}

pub struct Holder {
    z: SmallishType,
    ...
}

and we want now to implement a getter for Holder::z, should this getter return a reference or an owned value, i.e.

fn z(&self) -> &SmallishType { ... }

vs

fn z(&self) -> SmallishType { ... }

I personally like better the first option, especially if the copy type is larger than a handful of bytes, because you can always dereference and assign to copy it, if needed.

My personal rule of thumb is:

  • always implement Copy whenever possible, but
  • only do this for types which are small enough to be passed via registers (I use 3 words boundary in most cases)
  • always operate on Copy types by value

Sticking with those rules should not decrease performance and I like that I do not have to dereference values to operate on them, which I find (slightly) more readable.

2 Likes

Thank you for your answer, this is an interesting inside.

I am wondering what the harm would be if one implements Copy for types bigger than 3 words, but maybe use them as references where suitable?

Not implementing Copy for types larger than three words due to the size (and not due to the API promise) kind of contradicts the suggestion in std: Copy in std::marker - Rust

Another consideration is safety. If you return your value by reference, there's more chance to catch a problem if your object gets exclusively shared (mutable) and could be modified before the returned value is used. If you return a copied value, make sure there's no potential alias issue. I don't think that returning a reference of small Copy types makes any difference in the optimized code, anyway.

Sure, it's always possible to dereference the value on the other side, as it's possible to clone it when it's not Copy, but it's explicit and thus the user's responsibility.

I suppose that in most cases it's fine, though.

2 Likes

I virtually always use owned values for Copy-types, even for larger ones.

Rationale:

  • I assume, the compiler (hopefully) optimize some of these copying.
  • If the optimizer fails, copying happens mostly in L1 cache, which is fast (enough).
  • In tight loops, Copy-types tend to be small, so they fit into registers.
  • Source code ergonomic and simplicity is most important.

All these boils down to the basic rule: No premature optimization.

If there is a performance issue on the final result, as always: First measure, then identify the most valuable optimization target, and act on these measurements.

Don't waste (to much) time with assumptions, "angst" or minor performance penalties.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.