3 个因素导致现在的地址对齐约定:
- 生活很艰难
- 世界多姿多彩,世上有各种不同的人存在
- 但我们还是要在一起呀在一起
以下以最简,理想的模型进行讨论。
计算机的不同组件对“对齐”有不同的看法。比如一个最小存储单位为 8 字节的内存来说。访问地址1, 大小为 4 字节的数据。只需读取地址 0 的 8 字节的数据。然后在出口处移位下就行。因为只需在出口处做一次处理,所以可以不计成本进行移位优化。因为 RISC CPU 的设计,大多精简指令集的指令长就是字长。而指令还需区分取立即数和各种 action, 所以一字长的指令无法全部用来表示地址空间。所以大多 RISC CPU 强制地址对齐,地址的低位脑补成 0。顺便也减少了地址线的宽度。
访问内存的速度是非常非常非常……慢的。再加上 CPU 及其指令设计的限制。在这种艰苦条件下,我们必须无所不用其极地减少随机内存访问次数:
- 在一个访问周期里读写最多,但不更多的数据。也就是一字长大小的数据。
- 对于 CPU, 内存的最小存储单元的大小为最大,但不更大的 CPU 字长。
基于此,一般的 RISC CPU 的地址线宽度为
. 比如一个 32 位 CPU 的字长是 32bit, 字节大小为 8bit, 那么地址线宽度为
可选择
个地址单位, 每个单位的大小是 4 字节 于是总共可管理
字节的内存。这也是逻辑地址最低
位总是为 0 的来历了。对于 32bit 字长的 CPU, 就是最低 2 位为 0 了。注意,此段所述只是基于 de-facto 习惯(1 byte = 8 bit, mem 空间大小为
byte), 为了便于讨论而做的假设。并无特别的意义和强行规定。
以一个字长为 4 字节的 RISC CPU 来进行讨论。单位为字节。
此时,如果我们需要访问地址为 2 的大小为 1 字长也就是 4 字节的数据,也就是 2-5 的数据. 地址 2 mod 大小 4 = 2 不为 0. 这是未对齐的访问。(地址线以二进制表示。最后两位空置,所以始终为逻辑 0)我们需要将地址线设为 0(00) 读出 0-4, 取 2-3, 然后将地址线设为 1(00) 读出 4-7 取 4-5, 然后再合成所需数据。共两次访问。如果要访问地址为 0(00) 或者 1(00) 的大小为 1 字长的数据。则一次访问即可。
这就是地址不对齐导致访问变慢的来历了。
此时若要访问 地址 2 的半个字长大小的数据。也就是 2-3 的数据。我们可以将地址设为 0(00) 读出 0-3 的数据,然后将其在寄存器中右移 2 字节即可。
那么,问题来了。既然如此,我也可以访问地址 1, 大小为 2 字节的数据啊。也就是 1-2 的数据。将地址线设成 0(00) 读出数据,然后在寄存器内左移 1 字节,再右移 2 字节就行了啊。
这时候 1 mod 2= 1, 不为 0, 但需要访问内存的次数还是一次。
但世界上有许多地方,那儿的 CPU 字长只有 2 字节。当数据到那些地方去旅行时。那儿的 CPU 访问地址为 1, 大小为 2 字节的数据的时候还是需要两次的。
那么,问题又来了。字长为 4 字节的 CPU, 访问 8 字节长度的数据,这个数据反正始终都要读两次,那么它不对齐也是可以的呀。只要他的地址 mod 4 为 0 就可以了。
但世界上还有些地方,那儿的 CPU 字长是 8 字节的,当数据到那些地方去旅行时。那儿的 CPU 访问大小为 8 字节的数据,若其地址 mod 8 为 0 时,只要读一次就够了。此时读两次就是一种浪费了。
我们的世界是个艰难但又多姿多彩的世界。为了大家的数据都有一个兼容且一致的模型,方便交换,分析。我们郑重做出约定:
大小为 size 的字段,他的结构内偏移 offset 需符合 offset mod size 为 0.引用的 wikipedia 的第一段就是对这句话的精确表述。
最后,问题又来了。
struct hi {
let: 4 // padding 4
us: 8 // padding 0
play: 1 // padding 1
together: 2 // padding ?
}
together 字段的 padding 是要多少?是的 padding 0 就行了。所以大小是 8 + 8 + 2 + 2= 20
那为什么 gcc 告诉我们应该是 24 呢。
因为我们的世界不是孤单的世界。
数据们可以欢乐地组成团队。
如果我们不能相互体谅,自私地将最后的 padding 设为 0 的话。
假设第一个 hi 位于地址 0, 那么第二个 hi 就得从地址 20 开始了。此时 us 的地址是
, 而 28 mod 8= 4 不为 0.
如果 hi 的大小为其中最大单元的整数倍也就是 8 * 3= 24 的话。那么 第二个 hi 的 us 字段的地址是 24+8= 32. 而 32 mod 8= 0, 对齐了。所以,最后我们还需要 padding 4 字节。
在这个不孤单的世界里,为了同一类数据能和谐相处。所以我们郑重做出约定:
整个结构的大小必须是其中最大字段大小的整数倍。于是,不管是在一个数组里没羞没臊地在一起。还是在这个如此多姿多彩各不相同的世界里到处旅行。数据们的美好的生活都可以快速,和谐,一致地进行啦。
最后,若题主有闲,推荐看一下哈佛的 CS101, From NAND to Tetris 课程。从最简单的逻辑门开始,自己动手打造一遍 latch, flip-flop, register,RAM, ALU, CPU, assembler, compiler. 相信到时候你会有更深的体会。
====================================华丽的分隔符======================================================
以下是我个人的一点总结,字对齐的原因是为了能够一次读取当前的数据结构,但是由于不明确机器字长或者代码移植到别的机器的原因,字长是不固定,为了使在与字长与该结构相同的CPU一次读取当前值,而采用了对齐的方式,对于最后一个数据结构的对齐主要原因是考虑到数组的关系,所以选择了与最长数据结构的长度对齐。上文解决了困扰我许久的问题,在此表示发自内心的感谢。