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 对元组有特殊处理吗?
最佳答案
Why is it not 12 (4 + 8)? Does Rust have special treatment for tuples?
不。常规结构可以(并且确实)具有相同的“问题”。
答案是padding :在 64 位系统上,一个
f64
应该对齐到 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 的选项)。这是安全的,虽然它通常没有用,但在某些边缘情况下它很重要,我知道的那些(可能还有其他)是:
您也可以告诉
rustc
不使用 repr(packed)
填充结构.那风险更大,它通常会降低性能(大多数 CPU 与未对齐的数据交叉)并且可能会导致程序崩溃或在某些架构上完全返回错误的数据。这高度依赖于 CPU 架构,以及在其上运行的系统 (OS):每 the kernel's Unaligned Memory Accesses document
透明,但通常会有显着的性能成本。
发生。异常处理程序能够纠正未对齐的访问,
以显着的性能成本为代价。
发生,但异常不包含足够的信息
要纠正未对齐的访问。
默默地对请求的内存执行不同的内存访问,
导致难以检测的微妙代码错误!
因此,“1 类”架构将执行正确的访问,但可能会以性能为代价。
“第 2 类”架构将以较高的性能成本执行正确的访问(CPU 需要调用操作系统,并且未对齐的访问在软件中转换为对齐的访问),假设操作系统处理这种情况(它不t 总是在这种情况下,这将解析为 3 类架构)。
“第 3 类”架构将在未对齐访问时终止程序(因为系统无法修复它。
“Class 4”将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。
另一个常见的陷阱或未对齐的访问是它们往往是非原子的(因为它们需要扩展为一系列对齐的内存操作和对这些操作的操作),因此即使对于其他原子访问,您也可能会“撕裂”读取或写入.
关于rust - 为什么元组或结构的大小不是成员的总和?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65707981/