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