[SOLVED] Coming from OOP, stuggling to find a pattern in Rust

Looking at the source code, or_insert() and friends are annotated with the #[inline] attribute. This tells the compiler to embed a function's definition in the generated libstd.o so LLVM is able to inline it in downstream crates (which can also enable further optimisations) if deemed necessary.

So yes, in the final release build using these methods should be just as performant as if you wrote the match statements yourself.

1 Like

Ah, so in the case of or_insert_with(), no closure is even created if the Entry is Occupied. Got it -- thanks!

I still had one remaining bit of confusion -- would a closure even be created in the Vacant case, if those functions were inlined? I doubted it, but wanted to check. Is there a way to ask the compiler to output the inlined version of the code, so I could check such things for myself? 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.

And with regard to @jethrogb's comment, should the OP's code be shortened to

    fn get_environment(&mut self, name: &str, type_info: Option<TypeStmt>) -> &mut Environment<'a> {
        self.environments.entry(name.to_owned())
        .and_modify(|e| { if e.type_info.is_none() && !type_info.is_none() { e.type_info = type_info } })
        .or_insert_with(|| Environment::new(name.to_owned(), type_info.clone()))
    }

or do we for some reason really need to get the value again with

        self.environments.get_mut(name).unwrap()

at the end?

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.

2 Likes

It seems that you don't know how closures are desugared. @ExpHP explained this case very nicely, but if you want to see all the cases of how closures can be desugarred and take a peek inside, you can look at my blog post on the subject

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.