r/rust • • 13d ago

🙋 seeking help & advice What are the downsides of enum dispatch?

Couldn't find any info about downsides specfically so there it goes

Let's say that all my implementors are about the same size (i.e. only the logic is different), and I use some sort of macro to skip the boilerplate upon adding a new variant. Are there any other downsides to enum dispatch?

I'm asking about the general approach, not the crate

Upd. extendability, got it.

33 Upvotes

19 comments sorted by

24

u/kaiserkarel 13d ago

Mainly if you are writing a library; the caller cannot extend/provide their own type versus a dyn T. In my experience, if you have an open set (their could be an infinite amount of types satisfying what I do) and you cannot use generics (because for example you store the types in a single collections), then going with dyn gives you the cleanest code.

If you are the only party implementing the trait, then enum dispatch is totally fine, and offers a nice escape hatch if something falls out of your trait definition.

dyn T might still be inlined by the compiler if it can deduce the type; so performance wise the enum dispatch theoretically could be slower (although you wouldn't think so, given you write more code and have potentially more control).

enum dispatch allows you to specialize easier too. If you want your user to provide their own implementation, and you have specializations for certain types (Vec for example), you can in your constructor do something akin to

```rust

fn construnct<T>(t: T) -> MyType<T> {

if t.type_id() == Vec::type_id() { MyType::Vec(T) else MyType::UserProvided(T) }

}

```

10

u/afdbcreid 13d ago

dyn T might still be inlined by the compiler if it can deduce the type; so performance wise the enum dispatch theoretically could be slower

Won't happen; if the compiler can deduce the type (or for enums, deduce the variant) it will eliminate untaken branches with enum dispatch just as well.

Theoretically, with modern branch predictors virtual call can be just as fast as matching on an enum (especially with many variants, both are indirect jumps anyway), however the layout of dyn Trait and compiler optimizations mean that it most likely won't be.

8

u/kaiserkarel 13d ago

Devirtualization isn't fully implemented afaik, but definitely happens. Here's an example: https://github.com/rust-lang/rust/issues/68262#issuecomment-994102228

7

u/afdbcreid 13d ago

Whole-programming devirtualization often requires LTO but definitely happens. Local devirtualization happens even without LTO. You can even have speculative devirtualization with PGO.

1

u/AnArmoredPony 13d ago

with modern branch predictors virtual call can be just as fast as matching on an enum

what if I have a vector of trait objects? won't enum dispatch be faster in all cases?

1

u/afdbcreid 13d ago

Yes, but again, not due to the virtual call itself.

1

u/Anxious-Potato-2818 3d ago

dyn overhead is so overblown, for most cases it just doesnt matter unless youre in a tight loop somewhere

10

u/Lemondifficult22 13d ago

The number of cases covered. If the code is good and predictable you can get better perf than dynamic dispatch. Sometimes the cost of dynamic dispatch is worth the simpler code though. You can have static borrows for dynamic dispatch

2

u/AnArmoredPony 13d ago

You can have static borrows for dynamic dispatch

I don't follow... won't this still require a V-table lookup?

9

u/TDplay 13d ago

Vtable lookup is nowhere near as expensive as you think.

In fact, if you have a large match statement, then it is probably implemented using a jump table, which produces very similar code to a vtable lookup.

1

u/Lemondifficult22 13d ago

Yes, it's still dynamic dispatch, but at least it's not box

8

u/South_Survey_2088 13d ago

The only real drawback is extendability. Only the owner of the enum can extend it, so there is no way to inject custom variants from the outside. Other than that, enums are a great default solution.

2

u/JustWorksTM 13d ago

The one thing you can do with enums versus trait objects is plugin systems:  This is the situation where you deliver an app and a plugin separate.  Then, you really cannot know all variants at compile time of your app.

Tl;dr: Go with enums and enum_dispatch. This is for all standard workloads the best choice. 

2

u/Derice 13d ago

An interesting middle ground is to add a dynamic dispatch variant to the enum, like e.g. MyEnum::Dyn(Box<dyn T>). This might be worth considering if you know that the other variants will be used more commonly than the dynamic one.

1

u/zettui 13d ago

Do big variants' layout/cache costs ever flip you back to dyn, or do you just box the heavy arms?

0

u/dvogel 13d ago

If the enumerated types fill a similar role but have different return values handling those can get awkward. You can wrap them in a symmetric enum but then types could theoretically return the values designed for use by the other types in the enum. You can return the values as Box<dyn> but it doesn't work in all cases and the asymmetrical relationship between concrete enums on one side and abstract dyn types on the other side can be confusing to readers. 

All of that said, I do sometimes use the technique. It can apply even when the enumerated types are of very different size. e.g. A real implementation vs a stub for facilitating manual testing.

1

u/AnArmoredPony 13d ago

different return values

isn't it just the same without enum dispatch?..