Does partial application in Rust have overhead?

Viewed 2910

I like using partial application, because it permits (among other things) to split a complicated function call, that is more readable.

An example of partial application:

fn add(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let add7 = |x| add(7, x);

    println!("{}", add7(35));
}

Is there overhead to this practice?

Here is the kind of thing I like to do (from a real code):

fn foo(n: u32, things: Vec<Things>) {
    let create_new_multiplier = |thing| ThingMultiplier::new(thing, n); // ThingMultiplier is an Iterator
    let new_things = things.clone().into_iter().flat_map(create_new_multiplier);

    things.extend(new_things);
}

This is purely visual. I do not like to imbricate too much the stuff.

2 Answers

In your particular example, yes, extend can get inlined as a loop, containing another loop for the flat_map which in turn just puts ThingMultiplier instances into the same stack slots holding n and thing.

But you're barking up the wrong efficiency tree here. Instead of wondering whether an allocation of a small struct holding two fields gets optimized away you should rather wonder how efficient that clone is, especially for large inputs.

There should not be a performance difference between defining the closure before it's used versus defining and using it it directly. There is a type system difference — the compiler doesn't fully know how to infer types in a closure that isn't immediately called.

In code:

let create_new_multiplier = |thing| ThingMultiplier::new(thing, n);
things.clone().into_iter().flat_map(create_new_multiplier)

will be the exact same as

things.clone().into_iter().flat_map(|thing| { 
    ThingMultiplier::new(thing, n)
})

In general, there should not be a performance cost for using closures. This is what Rust means by "zero cost abstraction": the programmer could not have written it better themselves.

The compiler converts a closure into implementations of the Fn* traits on an anonymous struct. At that point, all the normal compiler optimizations kick in. Because of techniques like monomorphization, it may even be faster. This does mean that you need to do normal profiling to see if they are a bottleneck.

Related