Why does calling Result::as_ref() avoid doing a memcpy?

In this code for example:

struct BigStruct([u8; 10000]);

pub fn foo1() -> Result<(), bool> {
    baz(&blah()?); // does a memcpy?
    Ok(())
}

pub fn foo2() -> Result<(), bool> {
    baz(blah().as_ref().map_err(|x| *x)?); // no memcpy?
    Ok(())
}

unsafe extern "Rust" {
    safe fn blah() -> Result<BigStruct, bool>;
    safe fn baz(big_struct: &BigStruct);
}

The ? in foo1 triggers a memcpy of the BigStruct while the .as_ref().map_err(|x| *x)? in foo2 avoids it. See here on Compiler Explorer: Compiler Explorer.

I would expect that rustc would be able to see that BigStruct doesn't need to be moved in foo1 and generate the same code as foo2. There are no calls to as_ref or map_err in the output so it does appear to have sufficient inlining.

I noticed this while dealing with Result<DirEntry>/Result<Metadata>/Result<ReadDir> which depending on the platform are quite large because they contain an inline buffer for the file name resulting in expensive moves unless you .as_ref() first.

1 Like

Here are some “manually inlined” versions of your functions, that generate almost identical code (presented with extra vertical space so the Godbolt line-correlating highlighting can show more things):

pub fn foo1_expanded() -> Result<(), bool> {
    baz(&
        match blah() {
            Err(e) => return Err(e),
            Ok(bs) => bs,
        }
    );
    Ok(())
}

pub fn foo2_expanded() -> Result<(), bool> {
    baz(
        match (
            match blah() {
                Ok(ref bs) => Ok(bs),
                Err(ref e) => Err(e),
            }
        ) {
            Ok(bs) => bs,
            Err(e) => return Err(*e),
        }
    );
    Ok(())
}

Based on this, it seems to me that the key difference is probably that foo1 moves the BigStruct out of the Result before taking a reference to it, whereas foo2 takes a reference to the Result and never moves it.[1]

I agree that this seems like a missed optimization.


  1. Incidentally, note that Result::as_ref() doesn't contain that reference operator in its own function body; rather, the referencing happens as part of the method call operator in foo2 adapting the receiver type to be able to call as_ref() at all. Not that this matters for inlining. ↩︎

4 Likes

Thanks!

I guess a better phrasing of the question then is: Why on this line in foo1_expanded

            Ok(bs) => bs,

does it have to do a memcpy out of the Result, instead of reusing the existing memory for the Ok payload as the result of the overall match expression?