Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Rust Pointer Guide: Section 6 can be made clearer #19067

Closed
genbattle opened this issue Nov 18, 2014 · 7 comments · Fixed by #21031
Closed

The Rust Pointer Guide: Section 6 can be made clearer #19067

genbattle opened this issue Nov 18, 2014 · 7 comments · Fixed by #21031

Comments

@genbattle
Copy link
Contributor

Section 6 of The Rust Pointer Guide is as follows:

In many languages with pointers, you'd return a pointer from a function
so as to avoid copying a large data structure. For example:

struct BigStruct {
    one: int,
    two: int,
    // etc
    one_hundred: int,
}

fn foo(x: Box<BigStruct>) -> Box<BigStruct> {
    return box *x;
}

fn main() {
    let x = box BigStruct {
        one: 1,
        two: 2,
        one_hundred: 100,
    };

    let y = foo(x);
}

The idea is that by passing around a box, you're only copying a pointer, rather
than the hundred ints that make up the BigStruct.

This is an antipattern in Rust. Instead, write this:

struct BigStruct {
    one: int,
    two: int,
    // etc
    one_hundred: int,
}

fn foo(x: Box<BigStruct>) -> BigStruct {
    return *x;
}

fn main() {
    let x = box BigStruct {
        one: 1,
        two: 2,
        one_hundred: 100,
    };

    let y = box foo(x);
}

This gives you flexibility without sacrificing performance.

You may think that this gives us terrible performance: return a value and then
immediately box it up ?! Isn't that the worst of both worlds? Rust is smarter
than that. There is no copy in this code. main allocates enough room for the
`box , passes a pointer to that memory into foo as x, and then foo writes the
value straight into that pointer. This writes the return value directly into
the allocated box.

This is important enough that it bears repeating: pointers are not for
optimizing returning values from your code. Allow the caller to choose how they
want to use your output.

I was very confused by the second to last paragraph of this section. The paragraph uses the word "that" when referencing a memory location and a pointer. The first reference is clear, but the second isn't. This isn't helped by the fact that in the example the label x is used for both the variable passed into the function foo() and the parameter inside the function, making it hard to clearly express where and when values are being assigned. It also says the same thing twice

The disambiguation should make it clear that both the pointer to x and the pointer to y are being used in the function, where x is explicity passed as a parameter and y is implicitly passed as the return location. The general idea the paragraph expresses is already communicated, but I think it could be more explicit about the mechanics. Currently it almost makes it sound like foo() is returning a value via the passed parameter, or that it is moving the value rather than copying from one heap location to another.

Also someone mentioned that the "box" in the second paragraph may be missing another "".

@mahkoh
Copy link
Contributor

mahkoh commented Nov 18, 2014

You may think that this gives us terrible performance: return a value and then immediately box it up ?! Isn't that the worst of both worlds? Rust is smarter than that. There is no copy in this code. main allocates enough room for the `box , passes a pointer to that memory into foo as x, and then foo writes the value straight into that pointer. This writes the return value directly into the allocated box.

That's wrong / doesn't work / is not implemented. There might be a copy even if the function gets inlined.

@genbattle
Copy link
Contributor Author

This comment seems to disagree. According to the poster it works as described.

@mahkoh
Copy link
Contributor

mahkoh commented Nov 18, 2014

It doesn't: #18363

@huonw
Copy link
Member

huonw commented Nov 18, 2014

@mahkoh as written, the code does. This is the unoptimised IR of foo:

define internal void @_ZN3foo20h9f580e219f1e2e10paaE(%"struct.BigStruct<[]>"* noalias nocapture sret dereferenceable(24), %"struct.BigStruct<[]>"* noalias dereferenceable(24)) unnamed_addr #0 {
entry-block:
  %x = alloca %"struct.BigStruct<[]>"*
  store %"struct.BigStruct<[]>"* %1, %"struct.BigStruct<[]>"** %x
  %2 = load %"struct.BigStruct<[]>"** %x
  %3 = bitcast %"struct.BigStruct<[]>"* %2 to i8*
  %4 = bitcast %"struct.BigStruct<[]>"* %0 to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* %4, i8* %3, i64 24, i32 8, i1 false)
  br label %clean_custom_

return:                                           ; preds = %clean_custom_
  ret void

clean_custom_:                                    ; preds = %entry-block
  call void @"_ZN20Box$LT$BigStruct$GT$14glue_drop.108817h69e26cab4c19e281E"(%"struct.BigStruct<[]>"** %x)
  br label %return
}

In particular, there is only one stack slot used which is storing a pointer to BigStruct, not BigStruct itself, and the memcpy is from the input box straight into the out pointer.

@huonw huonw added the A-docs label Nov 18, 2014
@mahkoh
Copy link
Contributor

mahkoh commented Nov 18, 2014

I'm not saying that there are not (trivial) functions where this will work, e.g.,

fn f() -> X {
    unsafe { std::mem::zeroed() }
}

does work as expected without a copy. But this is not what the guide is saying:

This is important enough that it bears repeating: pointers are not for
optimizing returning values from your code.

This is wrong once f gets even a little complicated, e.g., the function in the other issue. Furthermore,

Arc::new(f())

is likely to copy the returned value twice: Once from f to the stack of Arc::new (even if Arc::new is inlined) and then onto the heap. Saying that "Rust is smarter than that" is misleading.

@steveklabnik
Copy link
Member

I wrote this section under @thestinger 's advisement, I'd like his thoughts here.

@Florob
Copy link
Contributor

Florob commented Nov 19, 2014

@mahkoh: Correct me if I'm mistaken, but I think the behaviour described here is distinctly different from named return value optimization. The point being made here is that the returned value is immediately placed on the heap and not temporarily on the stack.

E.g.:

extern crate test;

pub struct X {
    x: [u8, ..1 << 10],
}

pub fn f() -> X {
    let mut x: X = unsafe { std::mem::uninitialized() };
    for i in range(0, x.x.len()) {
        x.x[i] = i as u8;
    }
    x
}

fn main() {
    let x = box f();
    test::black_box(&x);
}

This will only take 1k of stack space + 1k of heap space, instead of 2k of stack space + 1k of heap space as would be the case without this optimization. With named return value optimization it would only take 1k of heap space. box exists precisely to prevent the issue you're describing with Arc. Eventually we hope to have box (ARC) f() (or at least I think that is the plan).

steveklabnik added a commit to steveklabnik/rust that referenced this issue Jan 12, 2015
alexcrichton added a commit to alexcrichton/rust that referenced this issue Jan 15, 2015
lnicola added a commit to lnicola/rust that referenced this issue Feb 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants