为什么元组或结构的大小不是成员的总和?

assert_eq!(12, mem::size_of::<(i32, f64)>()); // failed
assert_eq!(16, mem::size_of::<(i32, f64)>()); // succeed
assert_eq!(16, mem::size_of::<(i32, f64, i32)>()); // succeed

为什么不是 12 (4 + 8)?Rust 对元组有特殊处理吗?

回答

为什么不是 12 (4 + 8)?Rust 对元组有特殊处理吗?

不。常规结构可以(并且确实)具有相同的“问题”。

答案是填充:在 64 位系统上, anf64应该对齐到 8 个字节(即它的起始地址应该是 8 的倍数)。结构通常具有其最受约束(最大对齐)成员的对齐方式,因此元组的对齐方式为 8。

这意味着您的元组必须从 的倍数开始8,因此i32从 8 的倍数开始,以 4 的倍数结束(因为它是 4 个字节),并且编译器添加了 4 个字节的填充,以便f64正确对齐:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[ i32 ] padding [     f64     ]

“但是等等”,你喊道,“如果我反转元组的字段,大小不会改变!”。

确实如此:上面的架构并不准确,因为默认情况下rustc会将您的字段重新排序为紧凑的结构,因此它确实会这样做:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding 

这就是为什么您的第三次尝试是 16 个字节的原因:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] [ i32 ]

而不是 24:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[  32 ] padding [     f64     ] [  32 ] padding 

“抱紧你的马”你说,眼睛敏锐,“我可以看到 f64 的对齐方式,但为什么最后有填充?那里没有 f64!”

嗯,这样计算机就可以更轻松地处理序列:具有给定对齐方式的结构也应该具有与其对齐方式的倍数的大小,这样当您有多个它们时:

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
[     f64     ] [ i32 ] padding [     f64     ] [ i32 ] padding 

它们正确对齐并且如何放置下一个的计算很简单(只是被结构的大小抵消),它还避免了将这些信息放在任何地方。基本上,数组/ vec 本身永远不会被填充,而是填充在它存储的结构中。这允许打包成为结构属性,并且也不会感染数组。


使用该repr(C)属性,您可以告诉 Rust 完全按照您给出的顺序放置您的结构(这不是元组 FWIW 的选项)。

这是安全的,虽然它通常没有用,但在某些边缘情况下它很重要,我知道的那些(可能还有其他)是:

  • 与外来 (FFI) 代码接口,它需要一个非常具体的布局,这实际上是标志名称的来源(它使 Rust 表现得像 C)。
  • 避免高性能代码中的错误共享。

你也可以告诉rustc不垫结构使用repr(packed)

这风险更大,它通常会降低性能(大多数 CPU 与未对齐的数据交叉)并且可能会导致程序崩溃或在某些架构上完全返回错误的数据。这高度依赖于 CPU 架构,以及在其上运行的系统 (OS):根据内核的未对齐内存访问文档

  1. 某些体系结构能够透明地执行未对齐的内存访问,但通常会产生显着的性能成本。
  2. 当发生未对齐的访问时,某些架构会引发处理器异常。异常处理程序能够纠正未对齐的访问,但对性能的影响很大。
  3. 某些架构在发生未对齐访问时会引发处理器异常,但这些异常没有包含足够的信息来纠正未对齐访问。
  4. 某些架构无法进行未对齐的内存访问,但会默默地对请求的内存执行不同的内存访问,从而导致难以检测的微妙代码错误!

因此,“1 类”架构将执行正确的访问,但可能会以性能为代价。

“第 2 类”架构将以较高的性能成本执行正确的访问(CPU 需要调用操作系统,并且未对齐的访问在软件中转换为对齐的访问),假设操作系统处理这种情况(它不t 总是在这种情况下,这将解析为 3 类架构)。

“第 3 类”架构将在未对齐访问时终止程序(因为系统无法修复它。

“Class 4”将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。

另一个常见的陷阱或未对齐访问是它们往往是非原子的(因为它们需要扩展为一系列对齐的内存操作和对这些操作的操作),因此即使对于其他原子访问,您也可能会“撕裂”读取或写入.


回答

虽然@Masklinn 已经提供了一个普遍的答案,这是由于对齐,这里是 Rust 参考:

大小和对齐方式

所有值都有一个对齐方式和大小。

值的对齐指定了哪些地址可用于存储值。对齐值n只能存储在 的倍数的地址中n。例如,对齐为 2 的值必须存储在偶数地址,而对齐为 1 的值可以存储在任何地址。对齐以字节为单位,并且必须至少为 1,并且始终是 2 的幂。可以使用该align_of_val函数检查值的对齐情况。

值的大小是具有该项目类型(包括对齐填充)的数组中连续元素之间的偏移量(以字节为单位)。值的大小始终是其对齐方式的倍数。可以使用该size_of_val函数检查值的大小。

[...]

- Rust 参考 - 类型布局 - 大小和对齐

(强调我的)


以上是为什么元组或结构的大小不是成员的总和?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>