#traits #speed #performance #dynamic #optimization #variant-name

edyn

近10倍速度的动态调度方法调用替代品

1个不稳定版本

0.3.13 2024年7月26日

#16#variant-name

Download history 99/week @ 2024-07-21 30/week @ 2024-07-28 1/week @ 2024-08-04

每月130次下载

MIT/Apache

74KB
906

edyn

crates.io Docs License

edyn将您的trait对象转换为具体的复合类型,使它们的调用速度提高至10倍。

示例

如果您有以下代码...

// 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

...那么您可以使用edyn来提高其性能,如下所示

#[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

注意差异

  1. 新的枚举MyBehaviorEnum,其变体是简单地实现了trait MyBehavior的类型。
  2. 应用于枚举和trait的新edyn属性,将它们相互链接。
  3. 移除了Box分配。
  4. 更快的trait方法调用!

如何使用

  1. edyn添加到Cargo.toml依赖项,并在您的代码中使用use edyn::edyn
  2. 创建一个新的枚举,其变体是您定义的任何作用域内的trait实现者。
  3. #[edyn]属性添加到枚举或trait定义中。这将“注册”它到edyn库。注意应用到枚举或trait上的名称——我们将它称为FirstBlockName
  4. #[edyn(FirstBlockName)]属性添加到剩余的定义中。这将“链接”它与先前注册的定义。
  5. 更新您的动态类型以使用新的枚举。您可以使用任何trait实现者中的.into()来自动将其转换为枚举变体。

性能

更多关于性能的信息可以在文档中找到,并且基准测试结果可以在benches目录中找到。以下基准测试结果展示了使用edyn可以实现的性能。它们比较了在1024个随机具体类型的特质对象向量上重复访问方法调用的速度,使用了Box特质对象、&引用特质对象,或者edyn枚举类型。

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)

附加功能

序列化兼容性

虽然edyn是以性能为出发点构建的,但它所应用的转换使得所有数据结构对编译器的可见性大大提高。这意味着你可以在你的特质对象上使用serde或其他类似工具!

自动实现FromTryInto

edyn将为所有内部类型生成一个From实现,以便轻松实例化你的自定义枚举。此外,它还将为所有内部类型生成一个TryInto实现,以便轻松转换回原始的未包装类型。

属性支持

你可以在edyn变体上使用#[cfg(...)]属性来有条件地包含或排除相应的edyn实现。其他属性将直接传递到底层的生成枚举,允许与其他过程宏兼容。

no_std支持

edynno_std环境中受到支持。它非常适合嵌入式设备,在这些设备上,在栈上分配特质对象集合非常有用。

调整和选项

自定义变体名称

默认情况下,edyn将每个枚举变体扩展为具有与内部类型同名的单个未命名字段的枚举。如果你有某种原因想在edyn变体中使用特定类型的自定义名称,你可以像下面这样做到

#[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 */ },
}

枚举和特质需要自定义变体名称,这些名称也可以通过edyn进行优化。查看这个泛型示例以了解它是如何工作的。

一次性指定多个枚举

如果你想使用edyn为多个枚举实现相同的特质,你可以在相同的属性中指定它们

#[edyn(Widgets, Tools, Gadgets)]
trait CommonFunctionality {
    // ...
}

一次性指定多个特质

类似于上面,你可以使用单个属性为单个枚举实现多个特质

#[edyn(CommonFunctionality, WidgetFunctionality)]
enum Widget {
    // ...
}

泛型枚举和特质

edyn可以操作具有泛型参数的枚举和特质。在链接这些时,请确保枚举和特质的定义之间泛型参数的名称匹配,如下所示

#[edyn]
trait Foo<T, U> { /* ... */ }

#[edyn(Foo<T, U>)]
enum Bar<T: Clone, U: Hash> { /* ... */ }

对应泛型参数的名称应在枚举和特质的定义之间匹配。

这个示例更详细地展示了这一点。

故障排除

没有创建impls?

请注意不要忘记属性或输入链接属性时误输入名称。如果在找到链接属性之前解析已完成,则不会生成任何实现。由于宏系统技术限制,在此情况下无法正确警告用户。

无法解析枚举吗?

类型必须完全在作用域内才能用作枚举变体。例如,以下代码将无法编译

#[edyn]
enum Fails {
    crate::A::TypeA,
    crate::B::TypeB,
}

这是因为枚举必须在宏展开之前正确解析。相反,首先导入类型

use crate::A::TypeA;
use crate::B::TypeB;

#[edyn]
enum Succeeds {
    TypeA,
    TypeB,
}

技术细节

edyn是一个过程宏,以枚举的形式实现了一组固定类型的功能特性。这比使用动态分发要快,因为类型信息将“内置于”每个枚举中,避免了昂贵的虚表查找。

由于edyn是一个过程宏,它通过在编译时处理和扩展属性代码来工作。以下部分将解释上述示例可能如何被转换。

枚举处理

无法定义一个其变体是实际具体类型的枚举。为了解决这个问题,edyn通过为每个变体生成一个名称,并使用提供的类型作为其单个元组样式参数来改写其主体。对于大多数目的来说,每个变体的名称并不重要,但edyn目前仅使用提供的类型的名称。

enum MyBehaviorEnum {
    MyImplementorA(MyImplementorA),
    MyImplementorB(MyImplementorB),
}

特性处理

edyn实际上并不处理注解特性!然而,它仍然需要访问特性定义,以便记录特性的名称以及其中任何方法的函数签名。

特性实现创建

每当edyn能够“链接”两个定义时,它将生成一个impl块,为枚举实现特性。在上面的示例中,链接是通过MyBehavior特性定义完成的,因此将在该特性下方直接生成impl块。生成的实现块可能看起来像这样

impl MyBehavior for MyBehaviorEnum {
    fn my_trait_method(&self) {
        match self {
            MyImplementorA(inner) => inner.my_trait_method(),
            MyImplementorB(inner) => inner.my_trait_method(),
        }
    }
}

将相应地扩展额外的特性方法,额外的枚举变体将与每个方法定义中的额外匹配分支相对应。很容易看出,手动编写的代码会很快变得难以管理!

'From'实现创建

通常,如果没有知道其名称,将无法初始化新枚举的任何变体。然而,通过为每个变体实现From<T>,可以减轻这种要求。生成的实现可能看起来像以下这样

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)
    }
}

与上述情况一样,有大量可能类型变体会使手动维护变得非常困难。

注册和链接

任何熟悉编写宏的人都知道它们必须在本地处理,没有关于周围源代码的上下文。此外,syn中解析的语法项是!Send!Sync。这是出于很好的原因——在多线程编译和宏展开中,对于任何给定代码块的引用的顺序或生命周期没有保证。不幸的是,这也阻止了在单独宏调用之间引用语法。

为了方便起见,edyn 通过将语法转换为 String 并将其存储在 once_cell 惰性初始化的 Mutex<HashMap<String, String>> 中来绕过这些限制,其键是特例或枚举名称。

还有一个类似的 HashMap 专门用于“延迟”链接,因为不同文件中的定义可能会以任意顺序出现。如果一个带有(一个)参数的链接属性出现在对应的注册属性(无参数)之前,该参数将作为一个延迟链接被存储。一旦遇到该参数的定义,就可以像平常一样创建 impl 块。

由于链接延迟机制,在无法实现的情况下遇到链接属性不是错误。edyn 将期望在解析过程中稍后找到对应的注册属性。然而,在解析完所有原始源代码后,没有方法可以插入回调来检查所有延迟链接是否都已处理,这就解释了为什么无法警告用户有关未链接的属性。

依赖项

~290–770KB
~18K SLoC