Crates.io | edyn |
lib.rs | edyn |
version | 0.3.13 |
source | src |
created_at | 2024-07-26 09:51:53.608993 |
updated_at | 2024-07-26 09:51:53.608993 |
description | Near drop-in replacement for dynamic-dispatched method calls with up to 10x the speed |
homepage | |
repository | https://gitlab.com/antonok/edyn |
max_upload_size | |
id | 1315991 |
size | 116,954 |
edyn
transforms your trait objects into concrete compound types, increasing their method call speed up to 10x.
If you have the following code...
// We already have defined MyImplementorA and MyImplementorB, and implemented MyBehavior for each.
trait MyBehavior {
fn my_trait_method(&self);
}
// Any pointer type -- Box, &, etc.
let a: Box<dyn MyBehavior> = Box::new(MyImplementorA::new());
a.my_trait_method(); //dynamic dispatch
...then you can improve its performance using edyn
like this:
#[edyn]
enum MyBehaviorEnum {
MyImplementorA,
MyImplementorB,
}
#[edyn(MyBehaviorEnum)]
trait MyBehavior {
fn my_trait_method(&self);
}
let a: MyBehaviorEnum = MyImplementorA::new().into();
a.my_trait_method(); //no dynamic dispatch
Notice the differences:
MyBehaviorEnum
, whose variants are simply types implementing the trait MyBehavior
.edyn
attributes applied to the enum and trait, linking the two together.Box
allocation.edyn
as a Cargo.toml dependency, and use edyn::edyn
in your code.#[edyn]
attribute to either the enum or trait definition. This will "register" it with the edyn
library. Take note of the name of the enum or trait it was applied to -- we'll call it FirstBlockName
.#[edyn(FirstBlockName)]
attribute to the remaining definition. This will "link" it with the previously registered definition..into()
from any trait implementor to automatically turn it into an enum variant.More information on performance can be found in the docs, and benchmarks are available in the benches
directory.
The following benchmark results give a taste of what can be achieved using edyn
.
They compare the speed of repeatedly accessing method calls on a Vec
of 1024 trait objects of randomized concrete types using either Box
ed trait objects, &
referenced trait objects, or edyn
ed enum types.
test benches::boxdyn_homogeneous_vec ... bench: 5,900,191 ns/iter (+/- 95,169)
test benches::refdyn_homogeneous_vec ... bench: 5,658,461 ns/iter (+/- 137,128)
test benches::enumdispatch_homogeneous_vec ... bench: 479,630 ns/iter (+/- 3,531)
While edyn
was built with performance in mind, the transformations it applies make all your data structures much more visible to the compiler.
That means you can use serde
or other similar tools on your trait objects!
From
and TryInto
implementationsedyn
will generate a From
implementation for all inner types to make it easy to instantiate your custom enum.
In addition, it will generate a TryInto
implementation for all inner types to make it easy to convert back into the original, unwrapped types.
You can use use #[cfg(...)]
attributes on edyn
variants to conditionally include or exclude their corresponding edyn
implementations.
Other attributes will be passed straight through to the underlying generated enum, allowing compatibility with other procedural macros.
no_std
supportedyn
is supported in no_std
environments.
It's a great fit for embedded devices, where it's super useful to be able to allocate collections of trait objects on the stack.
By default, edyn
will expand each enum variant into one with a single unnamed field of the same name as the internal type.
If for some reason you'd like to use a custom name for a particular type in an edyn
variant, you can do so as shown below:
#[edyn]
enum MyTypes {
TypeA,
CustomVariantName(TypeB),
}
let mt: MyTypes = TypeB::new().into();
match mt {
TypeA(a) => { /* `a` is a TypeA */ },
CustomVariantName(b) => { /* `b` is a TypeB */ },
}
Custom variant names are required for enums and traits with generic type arguments, which can also be optimized by edyn
.
Check out this generics example to see how that works.
If you want to use edyn
to implement the same trait for multiple enums, you may specify them all in the same attribute:
#[edyn(Widgets, Tools, Gadgets)]
trait CommonFunctionality {
// ...
}
Similarly to above, you may use a single attribute to implement multiple traits for a single enum:
#[edyn(CommonFunctionality, WidgetFunctionality)]
enum Widget {
// ...
}
edyn
can operate on enums and traits with generic parameters.
When linking these, be sure to include the generic parameters in the attribute argument, like below:
#[edyn]
trait Foo<T, U> { /* ... */ }
#[edyn(Foo<T, U>)]
enum Bar<T: Clone, U: Hash> { /* ... */ }
The names of corresponding generic parameters should match between the definition of the enum and trait.
This example demonstrates this in more detail.
Be careful not to forget an attribute or mistype the name in a linking attribute. If parsing is completed before a linking attribute is found, no implementations will be generated. Due to technical limitations of the macro system, it's impossible to properly warn the user in this scenario.
Types must be fully in scope to be usable as an enum variant. For example, the following will fail to compile:
#[edyn]
enum Fails {
crate::A::TypeA,
crate::B::TypeB,
}
This is because the enum must be correctly parsable before macro expansion. Instead, import the types first:
use crate::A::TypeA;
use crate::B::TypeB;
#[edyn]
enum Succeeds {
TypeA,
TypeB,
}
edyn
is a procedural macro that implements a trait for a fixed set of types in the form of an enum.
This is faster than using dynamic dispatch because type information will be "built-in" to each enum, avoiding a costly vtable lookup.
Since edyn
is a procedural macro, it works by processing and expanding attributed code at compile time.
The folowing sections explain how the example above might be transformed.
There's no way to define an enum whose variants are actual concrete types.
To get around this, edyn
rewrites its body by generating a name for each variant and using the provided type as its single tuple-style argument.
The name for each variant isn't particularly important for most purposes, but edyn
will currently just use the name of the provided type.
enum MyBehaviorEnum {
MyImplementorA(MyImplementorA),
MyImplementorB(MyImplementorB),
}
edyn
doesn't actually process annotated traits!
However, it still requires access to the trait definition so it can take note of the trait's name, as well as the function signatures of any methods inside it.
Whenever edyn
is able to "link" two definitions together, it will generate an impl
block, implementing the trait for the enum.
In the above example, the linkage is completed by the MyBehavior
trait definition, so impl
blocks will be generated directly below that trait.
The generated impl block might look something like this:
impl MyBehavior for MyBehaviorEnum {
fn my_trait_method(&self) {
match self {
MyImplementorA(inner) => inner.my_trait_method(),
MyImplementorB(inner) => inner.my_trait_method(),
}
}
}
Additional trait methods would be expanded accordingly, and additional enum variants would correspond to additional match arms in each method definition. It's easy to see how quickly this can become unmanageable in manually written code!
Normally, it would be impossible to initialize one of the new enum variants without knowing its name.
However, with implementations of From<T>
for each variant, that requirement is alleviated.
The generated implementations could look like the following:
impl From<MyImplementorA> for MyBehaviorEnum {
fn from(inner: MyImplementorA) -> MyBehaviorEnum {
MyBehaviorEnum::MyImplementorA(inner)
}
}
impl From<MyImplementorB> for MyBehaviorEnum {
fn from(inner: MyImplementorB) -> MyBehaviorEnum {
MyBehaviorEnum::MyImplementorB(inner)
}
}
As with above, having a large number of possible type variants would make this very difficult to maintain by hand.
Anyone closely familiar with writing macros will know that they must be processed locally, with no context about the surrounding source code.
Additionally, parsed syntax items in syn
are !Send
and !Sync
.
This is for good reason -- with multithreaded compilation and macro expansion, there are no guarantees on the order or lifetime of a reference to any given block of code.
Unfortunately, it also prevents referencing syntax between separate macro invocations.
In the interest of convenience, edyn
circumvents these restrictions by converting syntax into a String
and storing it in once_cell
lazily initialized Mutex<HashMap<String, String>>
s whose keys are either the trait or enum names.
There is also a similar HashMap
dedicated to "deferred" links, since definitions in different files could be encountered in arbitrary orders.
If a linking attribute (with one argument) occurs before the corresponding registry attribute (with no arguments), the argument will be stored as a deferred link.
Once that argument's definition is encountered, impl blocks can be created as normal.
Because of the link deferral mechanism, it's not an error to encounter a linking attribute without being able to implement it.
edyn
will simply expect to find the corresponding registry attribute later in parsing.
However, there's no way to insert a callback to check that all deferred links have been processed once all the original source code has been parsed, explaining the impossibility of warning the user of unlinked attributes.