Rust 类型
类型在内存中的表示
对齐
如果没有内存对齐,可能导致两次访存的低性能和并发问题。比如 i64 分两次访问读或写;两次写指令之间,另一个线程可能执行对它的读操作
自然对齐-对齐大小等于类型的字节数。内建原始类型:u8 是单字节对齐,u6 双字节对齐,u32 是 4 字节对齐。。。
复杂类型的对齐大小等于它成员中最大的对齐大小。
布局
repr(C) 与 C/C++的布局一致。用在以下情况
与 C/C++的代码交互;
非安全代码下原始指针的操作;
相同字段不同类型的转换(cast);
下面这个 struct 占用多少字节?
1 + 3 + 4 + 1 + 7 + 8 + 2 + 6 = 26 bytes. 最后 6byte padding 是因为整个 struct 是 8 字节对齐的
如果上面代码使用 rust 的默认布局,占用多少字节?
将各个成员按照他们的字节大小逆序排序,没有 padding. 8 + 4 + 2 + 1 + 1
#[repr(align(n))] 紧凑布局--按代码原始顺序,没有 padding. 在内存吃紧或在慢速带宽传输的情况。
1+4+1+8+2=16 bytes
#[repr(align(n))] 自行指定较大的对齐字节数。避免 False sharing 问题。即不同的值连续存放到一块内存时,可以放到不同的 CPU 缓存行
repr(transparent) 用在某种类型只有一个字段,内部字段的布局与本身一样
复杂类型的布局
Tuple - 跟 struct 一样
Array -- 元素之间没有 padding 的连续序列
Union -- 各个成员的独立布局。选择成员中最大对齐数
Enum -- 跟 Union 相同,但是有一个隐藏的共享字段,其值用来区分各个成员的。
动态大小类型和宽指针
大部分类型在编译期间确定大小,即是 Sized
trait 对象和 slices, 如 dyn Iterator or [u8] 没有确定的大小。
用宽指针(或胖指针)代替未确定大小的对象。宽指针是 2 个字大小的指针。
对 trait object, 宽指针= 数据指针+虚拟方法表指针
对 slice; 宽指针=指针+长度
Box and Arc 支持宽指针, 如 Box
Trait 和 Trait 约束
编译和指派
如果定义一种使用泛型参数 T 的类型或函数, 编译器将会对每一个具体类型 T,实现改类型或方法代码的拷贝。类型中没有用到泛型的方法不会拷贝。缺点:
增加编译时间
增加了目标代码的大小
由于多份相同的指令,CPU 指令缓存失效
静态指派-- 在调用泛型化 trait 的方法中, 对于具体的 T,在编译期间就 知道该方法的地址。
有些不依赖泛型参数的代码块,可以提取出来作为内部方法。这样编译器不用对每种类型都拷贝一份相同的指令。
动态指派---调用时并不知道实现 trait 的具体类型是什么。
宽指针 &dyn trait 的另外一个地址指向 vtable。 vtable 保存具体类型的虚方法地址列表(包括内存布局和对齐),参见
std::task::RawWakerVTable
实现 trait 类型的 trait 对象能够动态指派. 如下图,
struct Cat
实现了trait Mammal
有些 trait 不能用于宽指针,不能转变为 trait object,即不是 object safety 的对象。 如 dyn Clone
trait 方法中有泛型参数
trait 方法中用到了 Self
类型 trait 有静态方法(第一个参数不能解引用为 Self 类型)。因为不知道到底调用哪个实例的方法
有 Self:Sized 约束的 trait。某个方法有 Self:Sized 约束,只是该方法不能用于动态分派。
节省编译时间,但是运行时需要在 vtable 中查找方法的地址比较费时。
一般说来, libary 中使用静态指派, binary 中用动态指派??
相干性(Coherence)和孤儿原则
相干性--对于某个给定的类型和方法,在该类型上只有唯一一种该方法的实现。比如你不能为 bool 类型定义 Display Trait,因为标准库已经实现。
孤儿原则: 在为某种类型实现某个 trait 时,至少 trait 或类型其中之一是本地 crate 定义的。即,你能够为第三方 typtes 实现本地的 trait,或者在第三方 crate 中为你定义的类型实现第三方定义的 trait。
孤儿原则的例外:
毯子(blanket)实现--可以用自定义的 trait 实现一大批的类型:impl MyTrait for T where T:。 切记:为已存在的 trait 定义覆盖性实现是一个 breaking change, 因为依赖它的 crate 可能已经实现,从而产生冲突
基础类型---可以为一些基础类型实现外部 trait. 基础类型是用 #[fundamental]标注过的类型,目前包括 &,&mut 和 Box. 比如
IntoIterator for &MyType
,IntoIterator 和 &都来自于标准库。覆盖实现--- 要使
impl<P1..=Pn> ForeignTraitfor<T1..=Tn> T0
被允许, 满足下面条件之一:至少存在一个 Ti 是本地类型 而且 第一个 Ti(包括)之前的 T 都不属于泛型参数 P1..=Pn;
或者泛型参数 Ps 可以出现在 T0...Ti 中,但是 Ps 被一些中间类型覆盖的话,如 Vec<T>
下面实现是合法的:
下面是不合法的实现:
标记(Marker)Trait
没有任何方法的 trait 是标记 Trait。 如 Send, Sync, Copy, Sized, Unpin. 除了 Copy 外,这些 trait 都是自动 Auto-trait,也就是说只要某类型的字段都实现某种标记 trait,编译器自动为这种类型实现这个标记 trait
没有任何数据和方法的类型称作标记类型。标记类型可以用来标记状态
Existential 类型
返回实现某个 trait 的类型, 这返回的类型就是existential类型。 如 IntoIterator 的 into_iter(self)方法。参见文章Rust Has Got Existential Types
版权声明: 本文为 InfoQ 作者【Shine】的原创文章。
原文链接:【http://xie.infoq.cn/article/85a821bc8d435f5e844895581】。文章转载请联系作者。
评论