Is there a way to ask the compiler to output the inlined version of the code, so I could check such things for myself?
Inlining is done by LLVM, not by rustc, so there is no "inlined version" of the code.
I still had one remaining bit of confusion – would a closure even be created in the Vacant case, if those functions were inlined? [...] I searched but came up dry. I ultimately used Compiler Explorer to investigate; it takes a bit of looking around, but apparently no closure gets created.
...well... okay, I'm not sure what precisely you were looking for in the generated assembly. But let's suppose that Rust did do its own inlining, at the syntax level. Your using_helpers
would be equivalent to:
pub fn using_helpers(map: &mut HashMap<String, i32>, key: String, v0: i32) -> i32 {
let closure_0 = |v: &mut i32| *v += 1;
let temp_0 = match map.entry(key) {
Entry::Occupied(entry) => {
closure_0(entry.get_mut());
Entry::Occupied(entry)
},
Entry::Vacant(entry) => Entry::Vacant(entry),
};
let closure_1 = || v0 + 1;
let temp_1 = match temp_0 {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(closure_1());
};
*temp_1
}
So yes, the closures are always created. But this is insanely cheap:
let closure_0 = |v: &mut i32| *v += 1;
This is a no-op. This closure doesn't borrow anything, so it is represented by a zero-sized type:
// equivalent to:
struct Closure0;
impl<'a> Fn(&'a mut i32) for Closure0 { ... }
let closure_0 = Closure0; // store of a constant of size 0; i.e. a no-op
The other one is almost as trivial. The closure borrows one local variable, so if we undo the "closure sugar", we see that an address gets taken of a single local:
let closure_1 = || v0 + 1;
// equivalent to:
struct Closure1<'a> {
v0: &'a i32
}
impl<'a> Fn() for Closure1<'a> { ... }
let closure_1 = Closure1 { v0: &v0 }; // store of an 8-byte address
LLVM will see this code storing addresses to local variables and chew right through it.
On the other hand, it might not have such an easy time folding the two match statements into one. (sure enough, the assembly for your using_helpers
appears to be quite a bit longer than for your using_match
, with more conditional jumps). So the costs associated with using helper functions are not necessarily negligible.
do we for some reason really need to get the value again with
self.environments.get_mut(name).unwrap()
at the end?
I think that was a flub. There might be some obscure scenarios where the longer version resolves some lifetime issues... but I don't have the type definitions to test this snippet with (and am too lazy to attempt to construct some). Anyways, the shorter version is preferred.