#collection #traits #vtable #iterator #dynamic #polymorphism #compile-time

zero_v

一个库,用于为实现通用特质的类型集合的函数输出实现迭代器,而不使用v表/动态多态

2个不稳定版本

0.2.0 2021年6月14日
0.1.0 2021年5月20日

#5 in #polymorphism

MIT/Apache

28KB
97

zero_v

Zero_V是在定义不使用动态多态的某些特质对象集合行为上的一个实验。这是一个包含一些辅助工具的小型crate,以及zero_v宏,该宏为您生成Zero_V生成的集合所需的特质和函数的样板代码。

如果以下所有条件都成立,它可能很有用

  • 库用户始终会在编译时知道类型集合的组成。
  • 库用户应该能够轻松地更改集合组成。
  • 表头开销很重要。

例如,让我们想象你已经编写了一个事件记录库,该库允许用户通过插件扩展它,以在记录之前更改事件。使用动态多态/表头,客户端代码可能如下所示

let plugins: Vec<Box<dyn Plugin>> = Vec![
    Box::new(TimestampReformatter::new()),
    Box::new(HostMachineFieldAdder::new()),
    Box::new(UserFieldAdder::new()),
];

let mut logger = EventLogger::with_plugins(plugins);

let events = EventStream::new();
for event in events.listen() {logger.log_event(event)};

这通常是处理问题的好方法。客户端设置简单,你很少会关心虚拟调用的开销。

但如果你确实关心开销,上述代码的Zero_V版本看起来像这样

use zero_v::{compose, compose_nodes};
let plugins = compose!(
    TimestampReformatter::new(),
    HostMachineFieldAdder::new(),
    UserFieldAdder::new()
);

let mut logger = EventLogger::with_plugins(plugins);

let events = EventStream::new();
for event in events.listen() {logger.log_event(event)};

对客户端来说,这里唯一的真正区别是使用了compose宏,去掉了集合中每个插件的装箱,以及额外的Zero_V导入。但从内部来看,你的类型现在是一般化的,没有使用装箱或表头定义的类型,这鼓励编译器对插件使用进行单态化,并删除虚拟函数调用。

将zero_v添加到项目中

要将zero_v添加到项目中,请将以下内容添加到您的Cargo.toml中的依赖项下

[dependencies]
zero_v = "0.2.0"

如果您是只需要创建集合的最终用户,或者您想自己实现特质的样板代码(如果您能避免,则不建议这样做,但如果您的特质涉及泛型或引用作为参数,则可能需要这样做),则可以使用带有zero_v属性宏的crate

[dependencies]
zero_v = { version = "0.2.0", default-features = false }

使用zero_v宏为您的类型实现Zero_V

如果您的特质不涉及具有生存期的参数或泛型,则zero_v宏会为您免费提供所有样板代码。您需要做的只是将其放置在特质定义之上

use zero_v::{zero_v};

// The trait_types argument tells the macro to generate all the traits you'll
// need for iteration over a collection of items implementing your trait.

#[zero_v(trait_types)]
trait IntOp {
    fn execute(&self, input: usize) -> usize;
}

// Passing the fn_generics arguments to the macro tells it to produce proper
// generic bounds for the second argument. The second argument takes the form
// <X> as <Y> where <X> is the name of your trait and <Y> is the name you're
// giving to the generic parameter which can accept a zero_v collection.

#[zero_v(fn_generics, IntOp as IntOps)]
fn sum_map(input: usize, ops: &IntOps) -> usize {
    ops.iter_execute(input).sum()
}

手动为您的类型实现Zero_V

要启用Zero_V,您需要将大量样板代码添加到您的库中。以下代码以简单示例逐步引导您完成这一过程。

use zero_v::{Composite, NextNode, Node};

// This is the trait we want members of client collections to implement.
trait IntOp {
    fn execute(&self, input: usize) -> usize;
}

// First, you'll need a level execution trait. It will have one method
// which extends the signature of your trait's core function with an extra
// paremeter of type usize (called level here) and wraps the output in an
// option (these changes will allow us to return the outputs of the function
// from an iterator over the collection.
trait IntOpAtLevel {
    fn execute_at_level(&self, input: usize, level: usize) -> Option<usize>;
}

// You'll need to implement this level execution trait for two types,
// The first type is Node<A, B> where A implements your basic trait and B
// implements the level execution trait. For this type, just
// copy the body of the function below, updating the contents of the if/else
// blocks with the signature of your trait's function.
impl<A: IntOp, B: NextNode + IntOpAtLevel> IntOpAtLevel for Node<A, B> {
    fn execute_at_level(&self, input: usize, level: usize) -> Option<usize> {
        if level == 0 {
            Some(self.data.execute(input))
        } else {
            self.next.execute_at_level(input, level - 1)
        }
    }
}
// The second type is the unit type. For this implementation, just return None.
impl IntOpAtLevel for () {
    fn execute_at_level(&self, _input: usize, _level: usize) -> Option<usize> {
        None
    }
}

// Next you'll need to create an iterator type for collections implementing
// your trait. The iterator will have one field for each argument to your
// trait's function, along with a level field and a parent reference to
// a type implementing your level execution trait and NextNode.
struct CompositeIterator<'a, Nodes: NextNode + IntOpAtLevel> {
    level: usize,
    input: usize,
    parent: &'a Nodes,
}

// Giving your iterator a constructor is optional.
impl<'a, Nodes: NextNode + IntOpAtLevel> CompositeIterator<'a, Nodes> {
    fn new(parent: &'a Nodes, input: usize) -> Self {
        Self {
            parent,
            input,
            level: 0,
        }
    }
}

// You'll need to implement Iterator for the iterator you just defined.
// The item type will be the return type of your function. For next, just
// copy the body of next below, replacing execute_at_level with the
// signature of your execute_at_level function.
impl<'a, Nodes: NextNode + IntOpAtLevel> Iterator for CompositeIterator<'a, Nodes> {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        let result = self.parent.execute_at_level(self.input, self.level);
        self.level += 1;
        result
    }
}

// Almost done. Now you'll need to define a trait returning your iterator
// type.
trait IterExecute<Nodes: NextNode + IntOpAtLevel> {
    fn iter_execute(&self, input: usize) -> CompositeIterator<'_, Nodes>;
}

// Finally, implement your iterator return trait on a composite over Nodes
// bound by NextNode and your level execution trait which returns
// your iterator.
impl<Nodes: NextNode + IntOpAtLevel>IterExecute<Nodes> for Composite<Nodes> {
    fn iter_execute(&self, input: usize) -> CompositeIterator<'_, Nodes> {
        CompositeIterator::new(&self.head, input)
    }
}

基准测试

以下列出了Zero_V的一些示例基准测试。源代码包含两组实现简单特质的对象,这些特质将usize转换为另一个usize(一个使用构造函数参数,另一个使用小的额外const优化),然后分别使用一个动态集合(标准vtable方式)和一个静态集合(使用Zero_V)对每组进行基准测试。以下是结果(硬件是联想T430,基准测试使用rustc 1.52.1编译,所以您的结果可能会有所不同)alt text Zero_V在这项基准测试中表现良好,但我想强调以下几点。

  • 这次使用的是特质,其中循环的每次迭代都执行非常少量的工作(单个乘法、加法、右移或左移操作)。这基本上意味着这些基准测试应该使Zero_V看起来尽可能好,因为相对于每次迭代的任务量,vtable开销将是最大的。
  • 每个用例都是不同的,每台机器都是不同的,编译器也可能很不可预测。如果性能足够重要,以至于要承担这种技术对你的代码造成的结构性成本,那么验证你是否得到了预期的加速,通过运行自己的基准测试套件,并确保这些基准测试反映在生产环境中,可能就足够重要。上面的基准测试还大量使用了内联注释来为特质实现提供注释,移除单个注释可以使执行速度慢三倍,所以值得探索在你的用例中内联(取决于你的性能需求)。
  • 细心的读者可能会注意到有一个第五个基准测试,基线,它在大约一纳秒内完成。这是省略特质和对象,仅有一个函数执行我们在其他基准测试中所做的任务(在我们的输入上执行一系列整数操作并求和输出)的基准测试版本。根据你的用例,设计你的API以便任何想要硬编码像那样优化的解决方案的人都有工具去做,可能是个好主意。如果你对编译器好,编译器也会对你好(偶尔的编译器错误除外)。

许可证:MIT OR Apache-2.0

依赖关系

约230KB