内存布局 Memory Layout

与其它数据类型相比, 枚举类型的内存布局比较复杂, 接下来我们分别说明.

只有枚举项标签

#![allow(unused)]
fn main() {
pub enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}
}

这里, Weekday 类型占用1个字节的内存.

weekday

随着元素个数的增加, 可以占用2个字节或者更多. 比如下面的例子, 编译器会给它分配2个字节, 因为有太多枚举项了:

use std::mem::size_of;

pub enum TooManyTags {
    Tag0,
    Tag1,
    Tag2,
    Tag3,
    ...
    Tag257,
    Tag258,
    Tag259,
}

fn main() {
    assert_eq!(size_of::<TooManyTags>(), 2);
}

显式地指定字节数

C++ 中, 可以显式地指定其数据类型, 以便确定占用的字节数. 下面的代码片段, Weekday 中的枚举项就会占用4个字节:

enum class Weekday : uint32_t {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday,
  Sunday,
};

Rust 中可以这样写:

use std::mem::size_of;

#[repr(u32)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum Weekday {
    #[default]
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

impl Weekday {
    pub const fn is_weekend(self) -> bool {
        matches!(self, Self::Saturday | Self::Sunday)
    }
}

fn main() {
    assert_eq!(size_of::<Weekday>(), 4);

    let monday = Weekday::Monday;
    let _tuesday = Weekday::Tuesday;
    assert!(!monday.is_weekend());
}

它们这些枚举项都占用4个字节, 尽管一个字节足够存储它们的值.

weekday-u32

混合类型

枚举项标签内, 还包含了其它类型的数据.

enum 也可以使用不同的类型作为其元素. 比如:

#![allow(unused_variables, dead_code)]

use std::mem::size_of;

pub enum WebEvent {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Click { x: i32, y: i32 },
}

impl WebEvent {
    fn tag(&self) -> u8 {
        unsafe { *(self as *const Self as *const u8) }
    }
}

fn main() {
    assert_eq!(size_of::<char>(), 4);
    assert_eq!(size_of::<WebEvent>(), 12);
    assert_eq!(size_of::<String>(), 24);

    let page_load = WebEvent::PageLoad;
    let keypress = WebEvent::KeyPress('a');
    let click = WebEvent::Click { x: 42, y: 43 };
    assert_eq!(click.tag(), 3);
}

这里, WebEvent 类型占用的内存, 基于其子元素所占内存的最大值, 这里就是 Click. 同时要考虑到内存对齐 (alignment) 的问题. 它里面的枚举ID只占用了一个字节, 但是还有3个字节作为填充(padding).

web-event

包含一个指针类型 - 空指针优化

enum 对于T里包含有指针类型时, 有独特的优化.

#![allow(unused_variables, dead_code)]

use std::mem::size_of;

pub enum WebEventWithString {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Paste(String),
    Click { x: i32, y: i32 },
}

impl WebEventWithString {
    fn tag(&self) -> u8 {
        unsafe { *(self as *const Self as *const u8) }
    }
}

fn main() {
    assert_eq!(size_of::<char>(), 4);
    assert_eq!(size_of::<WebEventWithString>(), 24);
    let mut s = "Hello, world".to_owned();
    s.reserve(3);
    println!("len: {}, cap: {}", s.len(), s.capacity());

    let paste = WebEventWithString::Paste(s.clone());
    let paste2 = WebEventWithString::Paste(s.clone());
    let click = WebEventWithString::Click { x: 42, y: 43 };
    let keypress = WebEventWithString::KeyPress('a');
    // assert_eq!(paste.tag(), 3);
    assert_eq!(click.tag(), 4);
    assert_eq!(size_of::<Option<String>>(), 24);
}

这里, Paste(String), 内部包含了一个 String 对象, 而 String 对象里有一个指针, 指向了堆内存, 用于存放字符串的内容.

下图就是这个枚举类的内存分布情况:

web-event2

可以看到, paste 对象第一个值包含的就是 String 对象里的那个指向堆内存的指针:

string-buf

同时, 除了 Paste(String) 之外的其它枚举项, 里面的枚举ID只占了一个字节, 还有7个字节用于填充(padding), 填充项的第7个字节, 被强制设置成了 0x80, 这个就是用于标记一个无效指针地址的, 即所谓的空指针优化.

比如 click 对象的枚举ID以及填充字节加在一起是8个字节. 它的值是: 0x80000000 00000004, 这不是一个有效的内存地址! 因为内存地址不高于 0x00007fff ffffffff. 表示这仅仅是一个枚举项的ID, 而不是一个内存地址. 除了 Paste(String) 外的其它几个枚举项, 都是这样的布局!

编译器就是利用这种手段, 在枚举中对有一个指针的对象做了优化. 像上面的例子中: assert_eq!(size_of::<String>(), size_of::<WebEventWithString>());

包含多个指针类型

如果一个枚举项中, 有多于一个枚举项里含有指针, 那个上面介绍的对指针的优化方法就不再适用了. 因为加 0x80 前缀的方法不能区别多个内存地址.

看下面的代码示例:

#![allow(unused_variables, dead_code)]

use std::mem::size_of;

pub enum WebEventWithMoreStrings {
    PageLoad,
    PageUnload,
    KeyPress(char),
    Paste(String),
    Copy(String),
    Click { x: i32, y: i32 },
}

impl WebEventWithMoreStrings {
    fn tag(&self) -> u8 {
        unsafe { *(self as *const Self as *const u8) }
    }
}

fn main() {
    assert_eq!(size_of::<char>(), 4);
    assert_eq!(size_of::<String>(), 24);
    let mut s = "Hello, world".to_owned();
    s.reserve(3);
    println!("len: {}, cap: {}", s.len(), s.capacity());

    assert_eq!(size_of::<Option<String>>(), 24);
    assert_eq!(size_of::<WebEventWithMoreStrings>(), 32);
    let paste = WebEventWithMoreStrings::Paste(s.clone());
    let copy = WebEventWithMoreStrings::Copy(s);
    let keypress = WebEventWithMoreStrings::KeyPress('a');
    let click = WebEventWithMoreStrings::Click { x: 42, y: 43 };
    assert_eq!(keypress.tag(), 2);
    assert_eq!(paste.tag(), 3);
    assert_eq!(copy.tag(), 4);
    assert_eq!(click.tag(), 5);
}

这个枚举类的内存反而简单了一些:

web-event3

因为不存在对指针的优化, Paste(String)Copy(String) 的枚举ID也被设置了, 它占用一个字节, 同时有7个字节的填充( padding).

这里, 使用 WebEventWithMoreStrings::tag() 方法就可以从各个枚举项里解析出枚举ID:

assert_eq!(keypress.tag(), 2);
assert_eq!(paste.tag(), 3);
assert_eq!(copy.tag(), 4);
assert_eq!(click.tag(), 5);