如何将Rust enum打包到最小大小?

lxkprmvk  于 2023-11-19  发布在  其他
关注(0)|答案(2)|浏览(170)

我有一个包含一些数据的Rust枚举。我想把它打包成尽可能少的字节。我试着像这样使用repr

#[repr(u8)]
enum MyEnum {
    OptionA(u32),
    OptionB(u32),
    Nothing,
}

fn main() {
    println!("{}", std::mem::size_of::<MyEnum>()); // prints 8 (should be 5)
}

字符串
理论上,这应该只需要占用5个字节(1个用于u8判别式,4个用于u32 s判别式)。但是,无论我使用什么repr,它都占用了整整8个字节,就好像它与4对齐一样。
官方的Rust文档清楚地表明,repr(u*)做了你对无字段枚举的期望,但是关于带字段的枚举的部分对我来说是模糊的:
如果枚举有字段,其效果类似于repr(C)的效果,因为有一个定义的类型布局。这使得将枚举传递给C代码,或访问类型的原始表示并直接操作其标记和字段成为可能。
所以布局已经定义好了,但是repr的参数没有做任何事情吗?或者这是一个bug?这对我来说似乎很疯狂。我知道对齐字段通常会更有性能,但是如果什么都不做,让你指定repr有什么意义呢?如果我想要内存有效的打包,我必须自己实现它,失去所有的rusts模式匹配和安全保证?

kcwpcxri

kcwpcxri1#

一般来说,这个类型必须是8字节。这是因为它包含一个u32,这是4字节,必须对齐到一个4字节的地址,如果你有一个数组,那么数组的元素必须每个对齐到4字节的倍数,这要求大小是4的倍数。否则,你可以对对象进行mut引用,而对象不会对齐,并且引用不允许未对齐。
不对齐的访问总是比较慢,在某些架构上,它也会用SIGBUS杀死进程。有些架构通常会让你的进程被杀死,但内核会以一个陷阱、一个上下文切换到内核、两次加载和一些移位的巨大代价来修复访问,然后从内核中切换上下文。通常这些架构上的人更喜欢SIGBUS,因为这样至少问题是显而易见的。即使是RISC-V,最新的架构之一,不能保证快速的非对齐访问(它可能会陷入内核)。
请注意,C编译器做同样的事情:

#include <stdio.h>
#include <inttypes.h>

struct foo {
    uint8_t tag;
    uint32_t value;
};

int main(void)
{
    printf("%zu\n", sizeof(struct foo));
}

字符串
的确,有些C编译器提供压缩表示,但它们是非标准的。
Rust中有一些关于打包枚举的讨论,但它们还没有标准化,因此不可用。

ttp71kqs

ttp71kqs2#

如文档所示,#[repr(u8)]对带有字段的枚举有两种影响:
1.有一个类型的定义布局。这使得将枚举传递给C代码,或访问类型的原始表示并直接操作其标记和字段成为可能。
1.如果判别式溢出了它必须适应的整数,它将产生编译时错误
所以它确实做了一些事情。虽然你似乎在追求的#[repr(packed)]目前不能在枚举上工作。
无论如何,这是一个 * 非常 * 的nieche repr,并带有严重的缺点,文档甚至指出它
是不能轻易使用的。除非你有极端的要求,否则不应该使用。
如果你不顾所有的警告和可能大约0的好处仍然想这样做,你可以使用一个手动标记的工会:

#[repr(u8)]
enum Discriminant {
    OptionA,
    OptionB,
    Nothing,
}
union Payload {
    value: u32,
    nothing: (),
}
#[repr(packed)]
struct MyEnum {
    discriminant: Discriminant,
    payload: Payload,
}

字符串
你甚至可以匹配它,但是创建一个对payload的引用是UB(因为引用必须对齐),所以要从中读取,你必须使用read_unaligned,但是对于任何packed数据结构都是如此,所以unsafe是不可避免的开始(而不是只为union访问引入的东西)。

相关问题