Dynamic dispach bloating binary

There's this widespread notion that using dynamic dispatch should reduce binary size (example). I've recently decided to make use of this optimization in our codebase. I came up with a clean wrapper that should make using dynamically dispatched collections easy, but the file sizes get larger, not smaller. I've tried it with smaller projects as well but they all grow in size after implementing dynamic dispatch. Is this whole "dynamic dispatch reduces file size" thing a myth, or am I doing something wrong?

I've made this small repo to demonstrate the issue. It has 3 modules that do the same thing, but one uses static dispatch, another uses dynamic dispatch explicitly, and the third uses my dynamic dispatch wrapper. Somehow, the generic version is much smaller than the others. file-bloat/lib.rs at main · MendyBerger/file-bloat · GitHub

Comparing Vec<u64> and Vec<Box<dyn Any>> is far from a fair fight, because you have significantly more allocations to do in the latter case. A closer comparison would be between Vec<Box<u64>> and Vec<Box<dyn Any>>, as those have the same basic shape.

However, you're not actually utilizing any dynamic dispatch here. The only reduction in monomorphization is that you're calling Vec::<Box<Any>>::push instead of Vec::<u32>::push, Vec::<u64>::push, and Vec::<char>::push, and Vec::<f64>::push. And most of Vec's allocation code is already manually polymorphic (independent of the generic type), so the benefit of avoiding monomorphization here is extremely minimal. Then if you're using DynWrapper, you're completely eliminating any benefit of polymorphization, because you're not even polymorphizing Vec, in addition to not doing any sort of dynamic dispatch on T. DynWrapper<T> is essentially no different to using Box<T> (except that it's slightly bigger due to carrying around a dyn vtable pointer).

When people say that dynamic dispatch reduces binary size, they're referring to the difference of e.g.

fn do_lots_of_stuff(iter: &mut dyn Iterator<Item=...>) {
    /* lots of code */

compared to making that generic; it's the difference between getting one copy of /* lots of code */ and one copy per type you call the function with.

Then if you're using DynWrapper, you're completely eliminating any benefit of polymorphization, because you're not even polymorphizing Vec

Since DynWrapper is repr(transparent), wouldn't you end up with Vec<Box<dyn Any>>?

A related conversation happened in another thread, and I threw together an example for build times

I just checked the file sizes for a release build

Monomorphic: 437 KB
Polymorphic: 232 KB

The layout is the same, but the code will still need to be monomorphized because it may use knowledge about that specific T type.

Shouldn't that T type be discarded? It's only used in a PhantomData.

But the compiler needs to prove that's true. It's not immediate by the fact that it's not actually stored in the fields since any code that uses a DynWrapper<T> could potentially depend on that T. Note that this could happen very subtly, for example some code very down the line could call std::any::type_name and get the name of the type you passed in, which includes the concrete type of T Rust Playground

1 Like

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.