关于字节对齐的深入理解网上有很多的好文章分析,这篇文章只对字节对齐做一个简单但较为全面的阐述,目的在于让很多初见字节对齐的学习者有一个基本的认识,知道什么是字节对齐,为什么要字节对齐以及如何用代码实现简单的字节对齐。
一、字节对齐
理解字节对齐,首先要知道计算机的内存空间是按照字节划分的,各种类型的变量数据也是按照其所占的字节大小存储在内存中。于此同时计算机读取内存并不是一个字节一个字节读的,以32位计算机为例,就是一次读取4个字节的内存,换句话说,如果一个4字节的数据刚好存储在计算机一次读取的4个字节空间中,那只需读取一次就可以拿到数据,但要是跨过两个4字节空间的边界存储,计算机就需要读取两次,浪费了时间。当然,这只是一个简单的例子,实际上会更加复杂,但也足以理解所谓“字节对齐”是干什么的了。
那什么是字节对齐呢?
正如上面的例子所展示的,字节对齐就是想办法让某个类型的数据(包括int等基本类型、自定义的结构体和数组等)存储在内存中时,不要出现某个数据跨过边界存储的情况。一般来说,基本类型的数据和数组只要存储的起始地址是其所占字节大小的整数倍就必满足此条件,不理解的下文会具体说明为什么。
比较复杂的就是结构体了,因为结构体成员的数据类型大小可能不同,因此整个结构体存储时会自动字节对齐,来保证所有成员数据都不会存储在边界上,而结构体成员声明的顺序是影响字节对齐大小的重要因素,下面是一个结构体两种不同的写法,经过字节对齐后两者所占空间大小是不同的。
struct Struct1
{
int a; //4字节
char b; //1字节
double c; //8字节
}
//Struct1 字节对齐后size为16字节,下面是图示
Struct1成员字节对齐的方式,一共16字节
---------------------------------------------------------
| int | char | | | | int占4字节,char占1字节
---------------------------------------------------------
| double | double占8字节
---------------------------------------------------------
struct Struct2
{
int a; //4字节
double c; //8字节
char b; //1字节
}
//Struct2 字节对齐后size为24字节,下面是图示
Struct2成员字节对齐的方式,一共24字节
---------------------------------------------------------
| int | | | | | int占4字节
---------------------------------------------------------
| double | double占8字节
---------------------------------------------------------
| char | | | | | | | | char占1字节
---------------------------------------------------------
为什么是这样的呢?
结构体的字节对齐,从上图就可看出来,每一行所占字节的大小必须对齐(和最大的数据类型有关);而且结构体成员是按顺序对齐的,即从上往下对齐。其实字节对齐有点像俄罗斯方块,有一个上限(最大字节类型的大小),你需要按顺序依次存放这些不同大小的方块(数据类型),从而使所占的空间最少。
Struct1中,最大的数据类型为8字节的double,所以一行的大小为8字节,先声明了int,所以先占了4字节,一行还剩8 - 4 = 4字节,而后面声明了char,占1字节,能放的下,一行还剩4 - 1 = 3字节,最后声明了double,占8字节,剩下的3字节放不下,就放在第二行,全部占满。因此Struct1使用了2行,一行8字节,就是16字节。
Struct2也是同理,读者可以自己推理一下,很简单,占24字节。
注意,结构体所占的字节大小和字节对齐的大小是两个不同概念,结构体的字节对齐大小就是上文例子所说的一行的字节大小(例子中为8字节)。可以看出是取决于其最大成员数据类型,而基本数据类型的大小往往是2的阶乘,如2、4、8、16等,因此结构体的字节对齐大小也是2的阶乘,同样的只要结构体首地址是其字节对齐大小的整数倍,就可以保证对齐。
二、如何字节对齐
前面说到了,无论何种类型的数据,其字节对齐的大小都是2的阶乘,即2、4、8、16.......要满足字节对齐大小的整数倍,以二进制来说:
字节大小为2 -> 首地址末尾有1个0;
字节大小为4 -> 首地址末尾有2个0;
字节大小为8 -> 首地址末尾有3个0
以此类推......
上面的说法应该不难理解,实在不明白自己用二进制写写看就明白了。
而在自定义设置栈空间的时候,栈顶的地址是需要字节对齐的,AAPCS 规则要求堆栈保持 8 字节对齐,这里不过多解释,那该如何用代码实现栈顶的8字节对齐呢?
//假设TopOfStack为栈顶地址
void align_stack(uint32_t * TopOfStack)
{
TopOfStack = TopOfStack & (~((uint32_t) 0x0007))
}
//0x0007 为 (uint32_t) (8 - 1),8为字节对齐的大小,可以换成4或16等
这是栈顶的向下8字节对齐,不会超出栈的内存空间。为什么呢?假设栈顶地址最后4位小于1000,为0XXX,那0XXX & 1000 = 0000,小于0XXX,即栈顶的地址,若栈顶地址最后4为大于1000,为1XXX,那1XXX & 1000 = 1000,依然小于1XXX。