How many bytes does a closure in size?

I having some code like this:

    pub fn single_flight<F: FnMut() -> Arc<T>>(&self, f: F) -> Arc<T> {
        match self.lock() {
            R::G(g) => {
                let any = f();
                unsafe { self.unlock(g, any.clone()) };
                any
            }
            R::V(any) => any,
        }
    }

I don't know how many bytes will copied for argument f to pass it to single_flight
Maybe it is just something like

struct closure {
    code_addr: usize,
    captured: *const (),
}

Or it is

struct closure {
    code_addr: usize,
    var1:  T,
    var2:  U,
    ...
}

e.g. the captured variables size make sense, and I should prefer

    pub fn single_flight<'a, F: FnMut() -> Arc<T>>(&self, f: &'a mut F) -> Arc<T> {
        match self.lock() {
            R::G(g) => {
                let any = f();
                unsafe { self.unlock(g, any.clone()) };
                any
            }
            R::V(any) => any,
        }
    }

than the implementation up above

The size depends on the size of the elements captured by the closure.

Some examples.

It is the second one. Rust tries hard not to do any implicit indirections.

Here, it's probably not a question of size (unless you profiled you code and found this place to be a bottleneck), but a question of semantics. Note also that &mut impl FnMut also implements FnMut (playground for illustration), so the first variant is strictly more general then the second.

To be pedantic, it isn't even the second. There is no address for the code of the closure in the closure at all, so that code_addr field doesn't exist. Calling a closure in Rust doesnt involve a "dynamic" function call to a function pointer in memory, instead it's a "static" call to the FnOnce/FnMut/Fn implementation of the closure type.

By the way, this means that the size of a closure that doesn't capture anything is zero. No value passed at runtime at all. The way that a function like single_flight still knows what closure code to call is the same way that generic functions in Rust always work: monomorphization. Every call to single_flight with a different closure type will generate its own version of the single_flight function itself. With the call to f() becoming a static call to the closure's code, though realistically, the closure's code will most often be inlined into that monomorphized instance of single_flight (especially if it's only called in a single place). And also, that monomorphized instance of single_flight is often inlined into the calling code as-well, especially if your passing a closure expression so that that call of single_flight is the only call with that same closure type. And after all the inlining, the closure struct itself might disappear at runtime even if it isn't zero-sized, because it doesn't cross any function boundaries anymore so the compiler doesn't need to pack everything up neatly into a singular closure struct at all, e. g. it may just leave all the captured variables in place where they came from and use them directly.


Of course, dynamic calling is also possible in Rust, by using trait objects, e. g. &dyn Fn(), but that's a separate topic. And for those, “the code_addr field” isn't contained in the struct either but instead part of a vtable for the trait object.

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.