为什么元组或结构的大小不是成员的总和?
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 类”架构将以较高的性能成本执行正确的访问(CPU 需要调用操作系统,并且未对齐的访问在软件中转换为对齐的访问),假设操作系统处理这种情况(它不t 总是在这种情况下,这将解析为 3 类架构)。
“第 3 类”架构将在未对齐访问时终止程序(因为系统无法修复它。
“Class 4”将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。
另一个常见的陷阱或未对齐访问是它们往往是非原子的(因为它们需要扩展为一系列对齐的内存操作和对这些操作的操作),因此即使对于其他原子访问,您也可能会“撕裂”读取或写入.
回答
虽然@Masklinn 已经提供了一个普遍的答案,这是由于对齐,这里是 Rust 参考:
大小和对齐方式
所有值都有一个对齐方式和大小。
值的对齐指定了哪些地址可用于存储值。对齐值
n只能存储在 的倍数的地址中n。例如,对齐为 2 的值必须存储在偶数地址,而对齐为 1 的值可以存储在任何地址。对齐以字节为单位,并且必须至少为 1,并且始终是 2 的幂。可以使用该align_of_val函数检查值的对齐情况。值的大小是具有该项目类型(包括对齐填充)的数组中连续元素之间的偏移量(以字节为单位)。值的大小始终是其对齐方式的倍数。可以使用该
size_of_val函数检查值的大小。[...]
- Rust 参考 - 类型布局 - 大小和对齐
(强调我的)