What does Box<dyn ...> actually allocate?

Hi there!

I'm currently struggling to understand what exactly is Box<dyn ...> allocating?

Using std::mem::size_of, I always get the same result for any dynamic type as it is based on the type itself. Same for values if I provide the type of a closure, which gives the following result:

assert_eq!(std::mem::size_of::<Box<dyn FnOnce()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn FnMut()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn()>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn(usize)>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn() -> bool>>(), 16);
assert_eq!(std::mem::size_of::<Box<dyn Fn(usize) -> bool>>(), 16);

More strangely though, getting the size of a closure from value always returns 0 (surely I'm doing something right here):

fn size_of_value<T>(_: &T) -> usize {
    std::mem::size_of::<T>()
}

let closure = |path: std::path::PathBuf| path.to_string_lossy().is_empty();

assert_eq!(size_of_value(&closure), 0); // What?

So I was wondering, first why is the second example not working as expected :stuck_out_tongue: and then how much space a Box<dyn ...> will actually take in memory, and not just on the stack, if I provide it a very large closure. Where will the different parts of the data be stored, the parts being:

  • The function's content (instructions) which I suppose remain in the program's binary and won't require an allocation each time ;
  • The function's pointer to indicate which should be called (which I suppose is part of the Box type);
  • The informations required for dynamic dipatch (which I suppose is part of the Box type)

Thanks in advance for your help :slight_smile:

EDIT: Re-reading this post I realize my goal isn't clear here ; I'm wondering if I can use Box with a very large closure, without it requiring a big heap allocation.

Yeah, so the size_of thing you are doing at the top of the post just measures the stack size of the Box, which is 16 because it is both a pointer and a vtable.

Additionally, it's completely correct that some closures have the size zero. This is because they compile down to a unique anonymous type whose call method has the contents of the closure. Of course, if you cast it to a function pointer, or have the closure capture a value, then it will no longer be size zero.

As for the size of an allocation of a Box<dyn Trait>, well it varies. It has the same size as the object you put inside it, so if you put a zero-sized closure into it, it doesn't allocate, but if the closure has captured a bunch of variables, then it could be larger.

2 Likes

Thanks for your answer!

So you're saying that for closures, as long as I use functions that don't capture anything from their environment, they'll always be compiled down to ZST and as such putting them into Box<dyn ...> won't require heap allocation, just stack allocation for storing the dynamic dispatch informations + the pointer?

It allocates a single contiguous buffer dynamically ("on the heap" if you prefer that terminology) for its wrapped value.

size_of::<Box<_>> is the size of the box, not the size of its heap buffer. The size_of function returns the size of its type parameter. Apart from necessarily being a compiler intrinsic, this function is not magic – it doesn't have special knowledge as to what potential heap buffers a type might allocate and point to. It only gives you the size of that type itself, which for a box is exactly one pointer (for Sized types) or two pointers (for dynamically-sized types).

If you are interested in the size of the contained type, ask for size_of::<T>() instead of size_of::<Box<T>>() for Sized types and size_of_val::<T>() for unsized types. If you are interested in approximate heap sizes, use the heapsize crate.

Correct.

Almost. A Box<dyn Trait> is made of two pointers:

  • one to the data (which points to no allocated memory if the type behind the trait is zero-sized);
  • and another one to the vtable required for dynamic dispatch.

In the case of a zero-sized closure, there's nothing to store for the data part, not even a function pointer. Calling closures is realized by means of the call_once(), call_mut(), etc. methods of the various Fn* traits, so the actual function pointer goes into the vtable part. This is also true when you don't use dynamic dispatch: unless you explicitly convert a closure to a function pointer, it will be statically dispatched and there isn't any function pointer business going on.

Finally, @Alice has already explained why some closures are zero-sized. If you capture variables, then you can indeed make non-zero-sized closures, in which case a very big closure can cause a very big heap allocation.

1 Like

Thanks for your answer :slight_smile:

So this means that, as long as the closure doesn't capture its environment, I can make it as big as I want, wrapping it inside a Box<dyn Fn...> won't require any heap allocation, right?

You mean its body, the actual executable code? Yes, that isn't part of the allocated buffer. Closures only need space for their captured environment.

1 Like

Perfect then!

Thanks for your help :smiley:

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.