Return static value in function to avoid heap allocation

I have the following definitions

    enum Score {
          SEND,
    }
    enum ScoreAtom {
        IntAtom(i64),
    }
    enum Node {
        Leaf(ScoreAtom),
    }

And I have a method return a constant value.

    fn object_score(&mut self) -> Option<Node<'a>> {
        Some(Node::Leaf(ScoreAtom::IntAtom(Score::SEND as i64)))
    }

Because the source code is long, I can't paste everything here, you may find full source code at here.

When object_score is called, where is the return value allocated? Stack or Heap?

Its size is known at compile-time, but it escapes the function scope, I am afraid Rust compiler will let it allocate from heap. Is that right?

As everyone knows, allocation on heap is much heavier than stack, and it is not friendly to CPU caches.

If the answer to above question is heap, I intent to optimize this method because it is on a frequent execution path.

I suppose to have a global static value to be reused on every return as below.

    static OBJECT_SCORE : Option<Node<'_>> = Some(Node::Leaf(ScoreAtom::IntAtom(Score::SEND as i64)));

    fn object_score(&mut self) -> Option<Node<'a>> {
        OBJECT_SCORE
    }

But it fails with following error

error[E0308]: mismatched types
  --> model/src/ordering/sort_send.rs:91:9
   |
91 |         OBJECT_SCORE
   |         ^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected enum `Option<ordering::Node<'a>>`
              found enum `Option<ordering::Node<'static>>`
note: the lifetime `'a` as defined on the impl at 87:6...
  --> model/src/ordering/sort_send.rs:87:6
   |
87 | impl<'a> SendScoreTreeIter<'a> {
   |      ^^
   = note: ...does not necessarily outlive the static lifetime

It is quite confusing, the static lifetime is certainly longer than any other lifetimes, why it rejects returning a 'static lifetime value? How can I fix it?

Sorry the full source code is too long to paste here. you may find them here.

Thank you in advance

Return values are stored on the stack or registers. Returning a struct/enum from a function is not very different than returning an integer.

The Rust compiler never allocates on the heap by itself. Heap allocation in Rust requires that you specifically request it using container types such as Box, Vec or String.

The fact that the value "escapes" the function is irrelevant. Rust values are… well, values. Structs and enums included. There are no implicit pointers going on, again, unless you specifically ask for indirection using references or smart-pointer-like types.

Returning a value from a function moves ownership of the value from the callee to the caller, by placing the value in the stack frame of the caller. This is a memcpy in the worst case, but even that is usually optimized away, and it doesn't require any heap allocation whatsoever.

1 Like

Thanks @eko @H2CO3

As I understand, stack is a continuous memory region, framed by stack pointer (ESP register in x86 CPU). When a function exits(scope ends), ESP pointer moves back to previous location before entering the current scope, so the on-stack values in current scope is inaccessible any more.

If a type can be passed through stack, from inner scope to its parent, then it should be able to pass by value.
e.g. In C++ we usually override = operator, define copy-constructor to make a type passable-by-value. The object instance is copied when current scope exits.
In Rust, I think a type has to implement Copy trait in order to pass it by value.

But in my code, I don't implement Clone trait nor Copy trait for them.
And the Node type actually is defined as below, I don't think they can be copied

enum Node<'a> {
    Leaf(ScoreAtom<'a>),
    Children(Box<dyn Iterator<Item = Node<'a>> + Sync + 'a>),
}

That's really amazing how Rust archives this.

No, it doesn't have to.

That's exactly what I was trying to explain above. Moving a value from one variable to another, or from one function to another, means a bitwise memcpy and invalidating the previous location. The compiler always performs a bitwise copy (at least conceptually) and enforces statically that the moved-from value isn't used anymore. This is why Rust doesn't need copy constructors, move constructors, or (implicit or explicit) deep cloning in order to pass anything by value.

3 Likes

In Rust, all values are relocatable; the Copy trait simply indicates that after a value has been “moved” (copied) to a new location, the old location will still contain a valid value. This isn’t a concern for function return values: All of a function’s local variables are deallocated as part of the function-return operation.

3 Likes

This isn't true. When using Pin, exactly that is prevented. Though the only contexts so far I've seen it used in is async code, for pinning futures.

1 Like

Pin sits in a slightly weird place: As far as I understand it, the compiler considers them to be relocatable, just like everything else. The API of Pin, however, is carefully designed to prevent the compiler from ever actually trying to move a pinned !Unpin type.

5 Likes