【问题标题】:Why does making one enum variant an `f64` increase the size of this enum?为什么将一个枚举变体设为“f64”会增加此枚举的大小?
【发布时间】:2019-12-06 15:01:12
【问题描述】:

我创建了三个几乎相同的枚举:

#[derive(Clone, Debug)]
pub enum Smoller {
    Int(u8),
    Four([u8; 4]),
    Eight([u8; 8]),
    Twelve([u8; 12]),
    Sixteen([u8; 16]),
}

#[derive(Clone, Debug)]
pub enum Smol {
    Float(f32),
    Four([u8; 4]),
    Eight([u8; 8]),
    Twelve([u8; 12]),
    Sixteen([u8; 16]),
}

#[derive(Clone, Debug)]
pub enum Big {
    Float(f64),
    Four([u8; 4]),
    Eight([u8; 8]),
    Twelve([u8; 12]),
    Sixteen([u8; 16]),
}

pub fn main() {
    println!("Smoller: {}", std::mem::size_of::<Smoller>()); // => Smoller: 17
    println!("Smol: {}", std::mem::size_of::<Smol>()); // => Smol: 20
    println!("Big: {}", std::mem::size_of::<Big>()); // => Big: 24
}

鉴于我对计算机和内存的理解,我期望它们的大小应该相同。最大的变体是大小为 16 的 [u8; 16]。因此,虽然这些枚举的第一个变体的大小确实不同,但它们的最大变体大小相同,并且变体总数相同。

我知道 Rust 可以做一些优化来确认某些类型何时存在间隙(例如,指针可能会崩溃,因为我们知道它们将无效并且为 0),但这实际上恰恰相反。我想如果我手动构建这个枚举,我可以将它放入 17 个字节中(区分只需要一个字节),所以 20 个字节和 24 个字节都让我感到困惑。

我怀疑这可能与对齐有关,但我不知道为什么,也不知道为什么有必要这样做。

谁能解释一下?

谢谢!

【问题讨论】:

  • 同样值得注意的是Arcs 也会发生这种情况(这实际上是我遇到这种行为的方式),所以虽然我知道指针和浮点数都有点特殊,但我不知道他们是如何联系起来的,这会导致这个问题。
  • 更简单的例子:play.rust-lang.org/…
  • 我还找到了一个很好的工具来帮助解决这个问题:rustc 带有一个print-type-sizes 调试标志,可以像这样运行:cargo +nightly-2019-07-19 rustc --release -- -Z print-type-sizes+nightly... 仅在以下情况下是必需的这不是您的默认设置)
  • 对于后代,here 是我在上面发布的命令输出的相关部分,用于我在原始问题中发布的示例。 (我更喜欢使用不太简单的示例,因为它更容易看出某些字段如何产生影响,而另一些则没有)

标签: enums rust


【解决方案1】:

大小必须至少为 17 字节,因为它的最大变体是 16 字节大,并且它需要一个额外的字节用于判别式(编译器在某些情况下可以很聪明,并将判别式放在变体的未使用位中,但这里不能这样做)。

另外,Big 的大小必须是 align f64 的 8 个字节的倍数。 8比17大的倍数是24。 同样,Smol 不能只有 17 个字节,因为它的大小必须是 4 个字节的倍数(f32 的大小)。 Smoller 只包含 u8 所以可以对齐到 1 个字节。

【讨论】:

  • 我明白了,所以它们之间的差异可以通过对齐来解释——f64 需要 8 字节对齐,f32 需要 4 字节对齐,u8 不需要任何对齐。我想我的下一个问题必须是......为什么整个事情必须对齐?您不能将第一个(或第二个;我们有 17 个)8 个字节视为对齐的浮点数而不对齐整个枚举吗?
  • 知道了——正如@rodrigo 所指出的(你链接到的页面也包括在内),整个结构/枚举必须对齐,因为如果它没有对齐,值的数组将有它们的内部成员不对齐。即使Big 以浮点数开头,如果整个枚举为 17 个字节,[Big] 中的下一个Big 将从第 18 个字节开始,因此 its 对齐将是错误的,甚至如果它的内部对齐是好的。感谢您和@rodrigo 的帮助!
【解决方案2】:

我认为是因为内部值的对齐要求。

u8 具有1 的对齐方式,所以一切都如您所愿,您得到的总大小为 17 个字节。

但是f32 的对齐方式是4(从技术上讲,它是依赖于拱门的,但这是最可能的值)。因此,即使判别式只有 1 个字节,您也会得到 Smol::Float 的这种布局:

[discriminant x 1] [padding x 3] [f32 x 4] = 8 bytes

然后对于Smol::Sixteen

[discriminant x 1] [u8 x 16] [padding x 3] = 20 bytes

为什么这个填充真的很有必要?因为要求类型的大小必须是对齐的倍数,否则这种类型的数组会错位。

同样,f64 的对齐方式为 8,因此您得到的完整大小为 24,即适合所有枚举的 8 的最小倍数。

【讨论】:

    【解决方案3】:

    正如mcarton 提到的,这是内部字段对齐和对齐/大小规则的影响。


    对齐

    具体来说,内置类型的常见对齐方式是:

    • 1:i8,u8。
    • 2:i16,u16。
    • 4:i32、u32、f32。
    • 8:i64、u64、f64。

    请注意,我说的是常见的,实际上对齐是由硬件决定的,在 32 位架构上,您可以合理地期望 f64 是 4 字节对齐的。此外,isizeusize 和指针的对齐方式会因 32 位和 64 位架构而异。

    一般来说,为了便于使用,复合类型的对齐方式是其任何字段的最大对齐方式,递归方式。

    对未对齐值的访问通常是特定于架构的;在某些架构上它会崩溃(SIGBUS)或返回错误数据,在某些架构上它会更慢(不久前的 x86/x64),而在其他架构上它可能会很好(在某些指令上是较新的 x64)。


    大小和对齐方式

    在 C 中,大小必须始终是对齐方式的倍数,因为数组的布局和迭代方式:

    • 数组中的每个元素都必须正确对齐。
    • 通过将指针递增sizeof(T) 字节来完成迭代。
    • 因此大小必须是对齐方式的倍数。

    Rust 继承了这种行为^1。

    有趣的是,Swift 决定定义一个单独的内在函数 strideof 来表示数组中的步幅,这允许他们从 sizeof 的结果中删除任何尾部填充。它确实引起了一些混乱,因为人们期望 sizeof 表现得像 C,但可以更有效地压缩内存。

    因此,在 Swift 中,您的枚举可以表示为:

    • Smoller: [u8 x 16][discriminant] => sizeof 17 bytes,strideof 17 bytes,alignof 1 byte。
    • Smol: [u8 x 16][discriminant] => sizeof 17 bytes,strideof 20 bytes,alignof 4 bytes。
    • Big: [u8 x 16][discriminant] => sizeof 17 bytes,strideof 24 bytes,alignof 8 bytes。

    这清楚地表明了 sizestride 之间的区别,它们在 C 和 Rust 中被混为一谈。

    ^1 我似乎记得一些关于可能切换到strideof 的讨论,正如我们所看到的那样没有实现,但找不到指向它们的链接。

    【讨论】:

    • >有趣的是,Swift 决定定义一个单独的内在函数 strideof 来表示数组中的步幅 我很高兴你提到了这一点,因为这对我来说真的很有趣。当然,今天早上,我一直在思考如何绕过它。在实践中,我不会费心(不值得;实际上这个大小很好),但我很高兴看到一些语言的这种行为得到了很好的支持。
    • C 没有这个要求。您混合 C 需求和机器需求来生成优化的代码。如果您要求编译器打包结构,您可以获得所有 3 个结构都有 17 个字节。 rextester.com/XNVHF93560,它仍然是正确的 C 代码。
    • @Stargateur:请注意,pack 也会更改对齐方式,因此您并没有否定我的观点。没有明确要求sizeof 返回一个是alignof AFAIK 倍数的数字。在 C++ 中,它可以从 5.3.3/2 (stackoverflow.com/a/4638295/147192) 推导出来。在 C 中,我希望 6.5.3.4/3 强制执行相同的语义:“当应用于具有结构或联合类型的操作数时,结果是此类对象中的字节总数,包括内部 和尾随填充" 结合对齐和数组。
    猜你喜欢
    • 1970-01-01
    • 2022-12-12
    • 2016-11-30
    • 2019-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多