Foveng
Foveng

Reputation: 47

Fastest way to "pass" a function to an other function

I am wondering currently what is the fastest way to pass a function to another function. By that I don't mean necessarily to pass it as an argument. Also let us assume that I know before compile time which kind of concrete functions I want to use in the generic function. The obvious way would probably be to pass it as an argument that implements one of the 3 closure traits, like the following:
fn foo(i: i32, f: impl Fn(i32)->i32)->i32{ f(i) }

With this approach I am not sure if f could for instance be inlined if I just pass a regular function. Am i correct if i think of the actual function passed as an instance of the type that is defined by its signature?

Another approach would be to define a dummy trait, that holds a function with the required signature:

trait dummy_trait{
    fn add(i:i32)->i32;
}

struct dummy_type{}

impl dummy_trait for dummy_type{
    fn add(i:i32)->i32{
        i+1
    }
}

fn add_outer<T:dummy_trait>(i: i32)->i32{
    T::add(i)
}

Then i could call it like this: add_outer::<dummy_type>(32); Like this i do not even have to pass the function as an argument. It should be able to for example inline here right? It just seems to be a little bit unwieldy, but i haven't found a better solution yet.

So my question is really if there is any performance difference between the two, and if so, if there is a nicer way to do the second option.

Upvotes: 0

Views: 588

Answers (2)

Kevin Reid
Kevin Reid

Reputation: 43773

You wrote

trait dummy_trait{
    fn add(i:i32)->i32;
}

struct dummy_type{}
impl dummy_trait for dummy_type{ ...

This is how function items (named functions) already work. You don't need to define your own trait to get the advantage that this seems to provide. Try this program:

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

fn foo<F: Fn(i32) -> i32>(i: i32, f: F) -> i32 {
    dbg!(std::mem::size_of::<F>());
    f(i)
}

fn main() {
    dbg!(foo(7, add));
}

It will print:

[src/main.rs:4] std::mem::size_of::<F>() = 0
[src/main.rs:9] foo(7, add) = 8

The size of add (as passed to foo) is 0 bytes, just like the size of dummy_type is 0 bytes. All the information about which code to execute by calling f() is provided at compile time by the function type F, not the value f.

(This is not true if you coerce add to a function pointer type, lowercase fn(i32) -> i32 — then, there is an actual pointer being passed around and called. The optimizer still might eliminate the pointer since it is a constant, though.)

Upvotes: 3

Masklinn
Masklinn

Reputation: 42282

Am i correct if i think of the actual function passed as an instance of the type that is defined by its signature?

No. While there are a few differences, fn(a: impl Trait) is mostly equivalent to fn<T: Trait>(a: T), so this call will be statically dispatched.

It's possible that if the function pointer goes through a ton of complex functions the compiler will miss the optimisation, but if you call e.g.

foo(5, add)

it's almost certain it'll inline the entire thing. You can easily test this behaviour on godbolt (the compiler explorer).

Upvotes: 1

Related Questions