为什么元组或结构的大小不是成员的总和?
Why is the size of a tuple or struct not the sum of the members?
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 本身永远不会被填充,而是填充在它存储的结构中。这允许 packing 成为结构 属性 而不会感染数组。
使用 repr(C)
属性,您可以告诉 Rust 完全按照您给出的顺序放置您的结构(这不是元组 FWIW 的选项)。
这是安全的,虽然它不是通常有用,但在一些边缘情况下它很重要,我知道的(可能还有其他)是:
- 与外部 (FFI) 代码接口,它需要一个非常具体的布局,这实际上是标志名称的来源(它使 Rust 表现得像 C)。
- 在高性能代码中避免 false sharing。
您还可以告诉 rustc
不要使用 repr(packed)
填充结构 。
风险更大,它通常会降低性能(大多数 CPU 与未对齐的数据相当交叉)并且 可能会使程序崩溃或 return 完全错误的数据 在某些架构上。这高度依赖于 CPU 体系结构和系统 (OS) 运行:根据 the kernel's Unaligned Memory Accesses document
- 一些架构能够执行未对齐的内存访问
透明,但通常会有显着的性能成本。
- 一些架构在未对齐访问时引发处理器异常
发生。异常处理程序能够纠正未对齐的访问,
以显着的性能成本为代价。
- 一些架构在未对齐访问时引发处理器异常
发生,但异常不包含足够的信息
未对齐的访问被更正。
- 有些架构不支持未对齐的内存访问,但可以
静静地对请求的内存执行不同的内存访问,
导致难以检测的细微代码错误!
所以“Class 1”架构将执行正确的访问,可能会以性能成本为代价。
"Class 2" 架构将以高性能成本执行正确的访问(CPU 需要调用 OS,未对齐的访问被转换为对齐访问 in software),假设 OS 处理这种情况(在这种情况下并不总是解析为 class 3 架构)。
"Class 3" 架构将在未对齐访问时终止程序(因为系统无法修复它。
"Class 4" 将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。
另一个常见的陷阱或未对齐访问是它们往往是非原子的(因为它们需要扩展成一系列对齐的内存操作和操作),因此您可能会“撕裂”读取或写入即使对于其他原子访问。
虽然 @Masklinn 已经 这是由于对齐,这里是 Rust 参考:
Size and Alignment
All values have an alignment and size.
The alignment of a value specifies what addresses are valid to store the value at. A value of alignment n
must only be stored at an address that is a multiple of n
. For example, a value with an alignment of 2 must be stored at an even address, while a value with an alignment of 1 can be stored at any address. Alignment is measured in bytes, and must be at least 1, and always a power of 2. The alignment of a value can be checked with the align_of_val
function.
The size of a value is the offset in bytes between successive elements in an array with that item type including alignment padding. The size of a value is always a multiple of its alignment. The size of a value can be checked with the size_of_val
function.
[...]
(强调我的)
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 本身永远不会被填充,而是填充在它存储的结构中。这允许 packing 成为结构 属性 而不会感染数组。
使用 repr(C)
属性,您可以告诉 Rust 完全按照您给出的顺序放置您的结构(这不是元组 FWIW 的选项)。
这是安全的,虽然它不是通常有用,但在一些边缘情况下它很重要,我知道的(可能还有其他)是:
- 与外部 (FFI) 代码接口,它需要一个非常具体的布局,这实际上是标志名称的来源(它使 Rust 表现得像 C)。
- 在高性能代码中避免 false sharing。
您还可以告诉 rustc
不要使用 repr(packed)
填充结构 。
风险更大,它通常会降低性能(大多数 CPU 与未对齐的数据相当交叉)并且 可能会使程序崩溃或 return 完全错误的数据 在某些架构上。这高度依赖于 CPU 体系结构和系统 (OS) 运行:根据 the kernel's Unaligned Memory Accesses document
- 一些架构能够执行未对齐的内存访问 透明,但通常会有显着的性能成本。
- 一些架构在未对齐访问时引发处理器异常 发生。异常处理程序能够纠正未对齐的访问, 以显着的性能成本为代价。
- 一些架构在未对齐访问时引发处理器异常 发生,但异常不包含足够的信息 未对齐的访问被更正。
- 有些架构不支持未对齐的内存访问,但可以 静静地对请求的内存执行不同的内存访问, 导致难以检测的细微代码错误!
所以“Class 1”架构将执行正确的访问,可能会以性能成本为代价。
"Class 2" 架构将以高性能成本执行正确的访问(CPU 需要调用 OS,未对齐的访问被转换为对齐访问 in software),假设 OS 处理这种情况(在这种情况下并不总是解析为 class 3 架构)。
"Class 3" 架构将在未对齐访问时终止程序(因为系统无法修复它。
"Class 4" 将对未对齐的访问执行无意义的操作,并且是迄今为止最糟糕的。
另一个常见的陷阱或未对齐访问是它们往往是非原子的(因为它们需要扩展成一系列对齐的内存操作和操作),因此您可能会“撕裂”读取或写入即使对于其他原子访问。
虽然 @Masklinn 已经
Size and Alignment
All values have an alignment and size.
The alignment of a value specifies what addresses are valid to store the value at. A value of alignment
n
must only be stored at an address that is a multiple ofn
. For example, a value with an alignment of 2 must be stored at an even address, while a value with an alignment of 1 can be stored at any address. Alignment is measured in bytes, and must be at least 1, and always a power of 2. The alignment of a value can be checked with thealign_of_val
function.The size of a value is the offset in bytes between successive elements in an array with that item type including alignment padding. The size of a value is always a multiple of its alignment. The size of a value can be checked with the
size_of_val
function.[...]
(强调我的)