21

I have some experience in C, but I'm new to Rust. What happens under the hood when I pass a struct into a function and I return a struct from a function? It seems it doesn't "copy" the struct, but if it isn't copied, where is the struct created? Is it in the stack of the outer function?

struct Point {
    x: i32,
    y: i32,
}

// I know it's better to pass in a reference here, 
// but I just want to clarify the point.
fn copy_struct(p: Point) { 
    // Is this return value created in the outer stack 
    // so it won't be cleaned up while exiting this function?  
    Point {.. p} 
}

fn test() {
    let p1 = Point { x: 1, y: 2 };
    // Will p1 be copied or does copy_struct 
    // just use a reference of the one created on the outer stack?
    let p2 = copy_struct(p1); 
}
1
  • Just to share a interesting article about similar subject blog.zgtm.de/1 Commented Feb 17, 2016 at 10:17

3 Answers 3

34

As a long time C programmer also playing with Rust recently, I understand where you're coming from. For me the important thing to understand was that in Rust value vs reference are about ownership, and the compiler can adjust the calling conventions to optimize around move semantics.

So you can pass a value without it making a copy on the stack, but this moves the ownership to the called function. It's still in the calling functions stack frame, and from a C ABI perspective it's passing a pointer, but the compiler enforces that the value is never used again upon return.

There's also return value optimization, where the calling function allocates space and the pointer is passed to the caller which can fill out the return value there directly. This is the sort of thing a C programmer would be used to handling manually.

So the safety of the ownership rules and borrow checker, combined with the lack of a fixed guaranteed ABI/calling convention, allow the compiler to generate efficient call sites. And generally you worry more about ownership and lifetime, then needing to try and be clever about function call stack behavior.

Sign up to request clarification or add additional context in comments.

Comments

0

I’m not sure what you are asking.

If your question is about what happens with the value you created from the point of view of you as a programmer, then the answer is it is moved (unless it implements Copy). You might want to go through some basic rust tutorials to grasp this concept.

If you are asking about what happens under the hood, then I’m afraid there might be no single answer. I believe, conceptually, the value is being copied using something like memcpy, but then the optimizer might kick in and eliminate this. I don’t think there is something like a specification regarding this, and it might be better to consider this an implementation detail.

3 Comments

"Large" return values are returned by reference; I'm not sure about parameters, but I wouldn't be surprised if "large" by-value parameters were passed by reference too.
@FrancisGagné How do you mean? Is the compiler smart enough to allocate memory for the return value on the caller’s stack in those cases?
Yes, and it happens even in debug builds, in order for the calling convention to be consistent. Even better, if you immediately put the result into a box, the code will actually perform the allocation first, then pass the pointer to the allocated memory and the function will write the return value there. Unfortunately, at the moment, that's only true with the unstable box keyword; using Box::new() doesn't enable that optimization. Placement syntax should hopefully fix that soon.
0

I did some experiments and found that rust will do return value optimization whenever the size of a struct is larger than 16, even if the struct has Copy trait.

#[derive(Copy, Clone)]
struct NoRVO {
    _a: [u8; 16],
}

#[derive(Copy, Clone)]
struct HasRVO {
    _a: [u8; 17],
}

#[inline(never)]
fn new_no_rvo() -> NoRVO {
    let no_rvo = NoRVO { _a: [0; 16] };
    println!("callee no_rvo:  {:p}", &no_rvo);
    no_rvo
}

#[inline(never)]
fn new_has_rvo() -> HasRVO {
    let has_rvo = HasRVO { _a: [0; 17] };
    println!("callee has_rvo: {:p}", &has_rvo);
    has_rvo
}

fn main() {
    let has_rvo = new_has_rvo();
    println!("caller has_rvo: {:p}", &has_rvo);

    let no_rvo = new_no_rvo();
    println!("caller no_rvo:  {:p}", &no_rvo);
}

An example of the output:

callee has_rvo: 0x13791ff380
caller has_rvo: 0x13791ff380
callee no_rvo:  0x13791ff2b0
caller no_rvo:  0x13791ff3e8

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.