0
votes

I've been reading questions like Why does a function that accepts a Box<MyType> complain of a value being moved when a function that accepts self works?, Preferable pattern for getting around the "moving out of borrowed self" checker, and How to capture self consuming variable in a struct?, and now I'm curious about the performance characteristics of consuming self but possibly returning it to the caller.

To make a simpler example, imagine I want to make a collection type that's guaranteed to be non-empty. To achieve this, the "remove" operation needs to consume the collection and optionally return itself.

struct NonEmptyCollection { ... }

impl NonEmptyCollection {
    fn pop(mut self) -> Option<Self> {
        if self.len() == 1 {
            None
        } else {
            // really remove the element here
            Some(self)
        }
    }
}

(I suppose it should return the value it removed from the list too, but it's just an example.) Now let's say I call this function:

let mut c = NonEmptyCollection::new(...);
if let Some(new_c) = c.pop() {
    c = new_c
} else {
    // never use c again
}

What actually happens to the memory of the object? What if I have some code like:

let mut opt: Option<NonEmptyCollection> = Some(NonEmptyCollection::new(...));
opt = opt.take().pop();

The function's signature can't guarantee that the returned object is actually the same one, so what optimizations are possible? Does something like the C++ return value optimization apply, allowing the returned object to be "constructed" in the same memory it was in before? If I have the choice between an interface like the above, and an interface where the caller has to deal with the lifetime:

enum PopResult {
    StillValid,
    Dead
};

impl NonEmptyCollection {
    fn pop(&mut self) -> PopResult {
        // really remove the element
        if self.len() == 0 { PopResult::Dead } else { PopResult::StillValid }
    }
}

is there ever a reason to choose this dirtier interface for performance reasons? In the answer to the second example I linked, trentcl recommends storing Options in a data structure to allow the caller to do a change in-place instead of doing remove followed by insert every time. Would this dirty interface be a faster alternative?

1
There's nothing special about self as compared to any other variable of any type.Shepmaster

1 Answers

3
votes

YMMV

Depending on the optimizer's whim, you may end up with:

  • close to a no-op,
  • a few register moves,
  • a number of bit-copies.

This will depend whether:

  • the call is inlined, or not,
  • the caller re-assigns to the original variable or creates a fresh variable (and how well LLVM handles reusing dead space),
  • the size_of::<Self>().

The only guarantees you get is that no deep-copy will occur, as there is no .clone() call.

For anything else, you need to check the LLVM IR or assembly.