深入理解 Rust 的动态分派模型 已翻译 100%

xiaoaiwhc1 投递于 2018/05/09 15:10 (共 14 段, 翻译完成于 05-10)
阅读 5866
收藏 17
2
加载中
让我做个开场白,我是rust世界的新手(虽然我喜欢它很久了),如果我犯了技术性错误,请让我知道并且我会尽力纠正它们。说到这为止,让我们开始吧!
kevinlinkai
翻译于 2018/05/09 15:10
0

在下面的代码片段中可以看到我深入研究动态分派的真正动机。假设我想创建一个包含trait对象数组的结构CloningLab(本例中为Mammal):

struct CloningLab {
    subjects: Vec<Box<Mammal>>,
}

trait Mammal {
    fn walk(&self);
    fn run(&self);
}

#[derive(Clone)]
struct Cat {
    meow_factor: u8,
    purr_factor: u8
}

impl Mammal for Cat {
    fn walk(&self) {
        println!("Cat::walk");
    }
    fn run(&self) {
        println!("Cat::run")
    }
}
Tocy
翻译于 2018/05/09 15:39
0

这段代码工作得很好。你能遍历对象集合并根据需要调用 run 或 walk 函数。然而,当你想再增加一个特性(trait)到特性对象绑定中时,事情开始变得麻烦了:

struct CloningLab {
    subjects: Vec<Box<Mammal + Clone>>,
}

impl CloningLab {
    fn clone_subjects(&self) -> Vec<Box<Mammal + Clone>> {
        self.subjects.clone()
    }
}

错误消息如下:

error[E0225]: only the builtin traits can be used as closure or object bounds
 --> test1.rs:3:32
  |
3 |     subjects: Vec<Box<Mammal + Clone>>,
  |
xiaoaiwhc1
翻译于 2018/05/09 17:29
1

这种情况出乎我的意料。我一直认为,多重绑定的特性对象(trait object)类似于C++中的多重继承,因此对象应该有多个虚指针,每个都指向对应的继承类,然后再根据匹配情况做分派。考虑到Rust毕竟还是一个非常年轻的语言,语言开发者们可能还不想过早地引入这种复杂性(为了小小的便利而仓促实现的糟糕设计将是沉重的负担),不过我想进一步研究一下这套系统是如何工作的(或者说不能工作的原因)。

xiaoaiwhc1
翻译于 2018/05/09 17:49
0

Rust中的虚函数表

像C++一样,Rust的动态分派也是通过一个函数指针表实现的(Rust文档中有说明)。根据那个文档,Cat中的Mammal特性对象(trait object)包含两个指针,内存布局如下:

xiaoaiwhc1
翻译于 2018/05/09 17:55
0

非常奇怪,从中可以看出,对象的数据成员也是通过一个指针来访问的(相当于多出一个间接层),并不像典型的C++内存布局那样。典型的C++内存布局如下:

C++内存布局中,数据成员是紧随vtable指针之后的(没有间接层)。Rust的实现方法比较有意思,当构造特性对象(trait object)时,它产生了一些额外的开销,这不同于C++的实现,C++的实现的好处是对象到基类指针的转换是无开销的(对于多重继承,仅有一点,后面有讲)。不过,Rust实现方式的开销毕竟很小,也确实带来一些好处:如果vtable在多态上下文中实际没有被使用,那它就不必存储它。我想这也可以被看作Rust不鼓励用多态的一个例证,因此这可能是个折衷。

xiaoaiwhc1
翻译于 2018/05/09 18:16
0

多重绑定的特性对象(trait object)

回到原来的问题,我们来考虑一下如何使用C++来解决。如果我们为一些结构实现了多个特性(就是抽象类),那么结构实例就会有如下的内存布局(以 Mammal和Clone为例):

xiaoaiwhc1
翻译于 2018/05/09 18:20
0

这里注意我们现在有多个vtable指针。每一个都指向Cat继承的一个对应的基类(Cat包含的那些虚函数)。将一个 Cat* 指针转换为 Mammal* 指针,没有任何开销,但是如果将一个 Cat* 指针转换为 Clone* 指针,编译器将给 this 指针增加一个8字节长度的偏移(这里假设 sizeof(void*) == 8)。(这里解释一下,看上图,Mammal*指针在Cat对象内存中排在第一位,这时Cat*与Mammal*其实是一个东西,内存地址是一样的,所以转换没有开销。后面Cat* 转 Clone*时,Clone* 指针排在 Mammal* 指针后面,所以this指针这时要加上一个偏移量才正确,偏移量就是 Mammal* 指针的大小)

可以想象一下Rust如何做:

xiaoaiwhc1
翻译于 2018/05/09 18:36
0

因此,现在特性对象包含两个vtable指针了。如果编译器需要实现对 Mammal + Clone 多重特性对象的动态分派,它能够在vtable中选择合适的入口并实现函数调用。可是因为 Rust 现在还不支持结构继承,那么如何决定正确的子对象作为 self 被使用,这个问题就不复存在了。self 将总是指向 data 指针所指向的东西。

xiaoaiwhc1
翻译于 2018/05/09 18:46
0

这样好像工作的还不错,可是这种方法也存在一些冗余开销。我们持有多个 size/align/drop指针 的拷贝。我们可以通过合并 vtables(虚函数表) 来消除这种冗余。这本质上跟你进行特性继承时编译器所做的一样:

trait CloneMammal: Clone + Mammal{}

impl<T> CloneMammal for T where T: Clone + Mammal{}

这种形式的特性继承(trait inheritance)也是规避特性对象(trait object)限制的一种推荐方法。使用特性继承生成一个没有任何冗余的单个vtable。内存布局如下:

xiaoaiwhc1
翻译于 2018/05/09 18:55
0
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(0)

返回顶部
顶部