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.
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):
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.
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?