A004-数据对齐的原因

来源:互联网 发布:node path resolve 编辑:程序博客网 时间:2024/04/19 11:01

今天读到一篇数据对齐的文章,明白了为什么需要数据对齐(地址对齐)。
不过作者的语言是属于作者自己的,我并不能立即理解他的全部表达。
只有我自己去思考推导之后才明白他的一些表达,因此我有必要将自己对这篇文章的解读记录下来。
原文:http://blog.csdn.net/tigerscorpio/article/details/5933807
.

地址对齐

数据对齐:数据A的存储地址的地址值 必须能整出 数据A数据长度
例如:
2Byte 的数据、就应该被存放在地址值2的倍数内存上。
4Byte的数据、就应该被存放在地址值4的倍数内存上。
所以,我们通常说 1字节的数据无论如何都是对齐的,2字节的数据需要2字节对齐4字节的数据需要4字节对齐
.

对齐的原因

(以32位机器为例、不考虑大小端)
数据需要对齐,是因为计算机在设计时,将32bit的数据分成4个1字节的数据,分别存放在4个内存芯片上,4个内存芯片共同构成一块最终的芯片
4个芯片共用一条地址总线,每个内存单元地址如下:
这里写图片描述
假如有数据A = 0x12345678,存放的地址0x00,是4字节对齐的。
那么这个4个字节会分别被存放在4块芯片Offset=0处:

0x12345678 0x12存放在Chip-0的Offset=0(地址0) 0x34存放在Chip-1的Offset=0(地址1) 0x56存放在Chip-2的Offset=0(地址2) 0x78存放在Chip-3的Offset=0(地址3)

CPU读取数据A时,会向地址总线发出 addr = 0x00地址值,由于4块芯片共用地址总线,所以4块芯片收到的地址值都是0x00,结果是都寻址4块芯片Offset=0处,一次寻址就读出32bit数据A
.
假如数据A 不是4字节对齐 存放 的,比如存放的地址0x01
那么这4个字节会分别被存放在4块芯片的如下位置:

0x12345678 0x12存放在Chip-1的Offset=0(地址1) 0x34存放在Chip-2的Offset=0(地址2) 0x56存放在Chip-3的Offset=0(地址3) 0x78存放在Chip-0的Offset=1(地址4)

读取数据A时,CPU地址总线发出 addr = 0x01地址值,由于4块芯片共用地址总线,所以4块芯片收到的地址值都是0x01,结果是都寻址4块芯片Offset=1处。
而芯片数据A第4个字节0x78 是在Chip-0Offset=1(地址4)处,而其他3个字节都不在这3块芯片Offset=1处、而是在它们的Offset=0 处,所以这次只正确地读出数据A第4个字节,这显然不是我们期待的结果。
要正确的取得数据A 的四个字节,CPU可以采取读两次的方式:

0x12345678 第一次读取:发送addr = 0x00,读出[Chip-0(地址0), 0x12, 0x34, 0x56] 第二次读取:发送addr = 0x01,读出[0x78, Chip-1(地址5), Chip-2(地址6), Chip-3(地址7)]

然后再将第一次读取出来的数据的第1个字节、替换成第二次读取出来的第1个字节
这样读取的效率会很低,要读两次,最后还要组合一下。
.
当然,还可以让4块芯片不共用一条地址总线,而是使用独立的4地址总线
一次性的发送4地址值,直接定位到4块芯片4个字节去,这样CPU一次就可以读出4字节数据A了。
但是地址总线的数量会增加到原来的4倍,也就是地址线IO和配套的寄存器都增加到原来的4倍,还得修改地址译码的控制器。
这个成本是很高的,所以设计CPU的厂家也不这样干,就要设计成地址对齐

因此地址对齐的要求就是这样来的,都是CPU设计成了这样,所以就变成了这样,目的是为了让CPU的结构更简洁。

如果我们的数据不对齐,会出现以下三种结果:

不对齐的后果 (1). CPU读到错误的数据,但CPU不报错,这个我没接触过 (2). CPU直接break,抛出一个hardfault (3). 据说x86的CPU可以采用 读2次再组装的方式 来读取 没对齐的数据,这个我也没接触过

我之前用的一款cortex-M0的芯片、一读到没对齐的数据,CPU就跳转到hardfault中断,用户程序只能调用CHIP-RESET指令重启芯片或者关机。这个芯片就是这样直接,强制罢工,强制用户将数据对齐
.

对齐规则

(假设4字节对齐,测试环境:Visual Studio 2012)
每个数据对象都有一个对齐限制,这个概念称为数据对象的对齐模数

基本数据类型的对齐模数如下:

数据类型 对齐模数 char / uint8_t 1字节 short / uint16_t 2字节 int / uint32_t 4字节 float / uint32_t 4字节 double / uint64_t 8字节

编译器指定的对齐模数和这些基本类型本身的对齐模数不一致时,选择较小的一个模数。
.
比如编译器要求4字节对齐,那么:

数据类型 数据的对齐模数 char / uint8_t 1字节 short / uint16_t 2字节 int / uint32_t 4字节 float / uint32_t 4字节 double / uint64_t 4字节

.
假如编译器要求2字节对齐,那么:

数据类型 数据的对齐模数 char /uint8_t 1字节 hort/uint16_t 2字节 int /uint32_t 2字节 float/uint32_t 2字节 double/uint64_t 2字节

.

结构体的对齐

对于结构体/联合体/数组类型,它的对齐模数是其内部各成员对齐模数最大值
对于结构体/联合体,还要求他占据的空间对齐模数的倍数
例如:

struct student{    char data;    int  number;    char addr;}stu;

它的3个成员的对齐模数各自为:

数据类型 对齐模数 char data 1字节 int number 4字节 char addr 1字节

所以student对齐模数4
student在内存中的存储情况如下:
这里写图片描述
data 存放在Offset=0
接着空余3字节,因为这3个字节的地址都不是4对齐的。
number 的存储需要4字节对齐,所以number只能存放在Offset=4处。
addr 存放在Offset=8处。
接着空余3字节,因为student对齐模数4,他占据的空间被要求是4的倍数
所以空余3个字节后,student 的总大小从9字节增加到12字节,是4的倍数了。
因此student 一共占据了12字节的空间。
.
显然,将addr 放置在data 的后面,将number 放在addr 的后面,这样student占据的空间就降低到8字节
因为addr1字节对齐,可以被存放在Offset=1处:
这里写图片描述
.

位域的对齐

对于结构体中的位域的对齐,还要看其内部成员的类型和大小。
例如:

struct flame_data{    char a : 3;    char b : 3;    int  c : 2;    char d : 4;    char e : 5;}IR_data;

变量ab 的类型相同,而且他们一共才6bit,可以存放在同1个字节中,因此ab 共同占据1个字节
变量c2bit,和前面的ab 一共刚好是8bit,但是他们类型的长度不同,所以不能合并存储到同1个字节中,同样c 也不能和d 合并,只能单独存储。
同时c 要对4字节对齐,所以在ab 占据的那1个字节之后,还需要空出3个字节,让c 存储到到地址对齐后的第4个字节
变量de 类型相同,但是他们加起来的长度超过了8bit,因此也不能合并到同1个字节中,因此de 各自占据1个字节
这里一共占据10个字节
最后、flame_data 作为结构体,它占据的空间长度还需要是其对齐模数的倍数
flame_data对齐模数4,所以在上面的10字节后面还需要增加2字节,以整除4
因此flame_data 最终一共占据12个字节
.

特殊的位域

位域还有2个特殊的地方:
(1). 未命名的域
例如:

struct flame_k{    char a : 3;    char   : 2;    char b : 3;}k_data;

3个域可以被合并存储到同1个字节(类型一致,总大小没超过8bit),但未命名的第2个域不能被引用。

(2). 大小为0的域
例如:

struct flame_j{    char a : 3;    char   : 0;    char b : 3;}j_data;

大小为0的域用来将它的前后两个域隔开禁止他们合并到一起。
所以这个位域一共占据2个字节
.

pragma pack( n )

32位机器默认是4字节对齐,但我们可以使用#pragma pack(n) 来指定n字节对齐
#pragma pack(4) 将对齐模数人为指定为4字节对齐
#pragma pack() 用来取消刚才设置的自定义对齐模数、采用编译器默认的对齐模数

0 0
原创粉丝点击