首页 > 代码库 > 字节对齐问题详解

字节对齐问题详解

字节对齐,我的理解就是用空间换取时间,提高存取的效率。下面详细分析:
1.什么是字节对齐?

现代计算机中内存空间都是按照字节划分的,从理论上讲似乎对任何类型变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是字节对齐。

注意:我们经常听说的对齐在N上,它的含义就是数据的存放起始地址%N==0。

2.原因及其意义:

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。

struct Test
{
    char ch;
    int  in;
}test;

假设变量test存放在内存中的起始地址为0x00,那么其成员变量ch的起始地址为0x00,成员变量i的起始地址0x01,变量test一共占用了5个字节。当CPU要对成员变量ch进行访问时,只需要一个读周期即可。而如若要对成员变量in进行访问,那么情况就变得有点复杂了,首先CPU用了一个读周期,从0x00处读取了4个字节(注意由于是32位架构),然后将0x01-0x03的3个字节暂存,接着又花费了一个读周期读取了从0x04-0x07的4字节数据,将0x04这个字节与刚刚暂存的3个字节进行拼接从而读取到成员变量in的值。为了读取这个成员变量in,CPU花费了整整2个读周期。试想一下,如果数据成员i的起始地址被放在了0x04处,那么读取其所花费的周期就变成了1,显然引入字节对齐可以避免读取效率的下降,但这同时也浪费了3个字节的空间(0x01-0x03),即用空间换取了时间。


下面我们讲三个重要概念:自身对齐值,指定对齐值和有效对齐值

自身对齐值:即数据类型的自身的对齐值。例如char型的数据,其自身对齐值为1字节;short型的数据,其自身对齐值为2字节;int,float,long类型,其自身对齐值为4字节;double类型,其自身对齐值为8字节;而struct和class类型的数据其自身对齐值为其成员变量中自身对齐值最大的那个值。

指定对齐值:#pragma pack (value)时指定的对齐值value。

有效对齐值:min(自身对齐值,指定对齐值)。


3.准则:

其实字节对齐的细节和具体编译器实现有关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

4.举例分析:
对于结构体struct:
#include <stdio.h>
#include <iostream>
using namespace std;

struct A {  
    char c;  
    int i;  
    short s;  
};  
   
struct B {  
   char c;  
   short s;  
   int i;  
};  
void main()
{
	cout << sizeof(struct A) << " " << sizeof(struct B) << endl
首先来看sizeof(struct A),假设A的起始地址为0x00,做这样的假设只是为了更方便理解,其实A始终被放在对齐边界上,这并不影响sizeof的结果,在接下来的例子中,我们也会继续沿用这个假设。言归正传,数据成员c的自身对齐值=1,指定对齐值=4(默认),所以其有效对齐值为1,因0x00%1==0,所以它被存放在0x00处;数据成员i的自身对齐值=4,指定对齐值=4,可得出其有效对齐值为4,因0x01%4 != 0,因此它应该被存放在0x04地址处,占用0x05,0x06,0x07共4个字节;接下来看数据成员s的自身对齐值=2,指定对齐值=4,得出有效对齐值为2,因0x08%2 == 0,因此它被存放在起始地址为0x08处,并占用2字节;最后再看数据结构A自身的对齐值=4(最大数据成员自身对齐值),指定对齐值=4,得有效对齐值为4,因0x0A%4 != 0,因此多占用0x0A和0x0B为结构体A所用(这一步的作用是基于结构体数组的出发,对于结构体或者类,要将它们补充成其有效对齐值的整数倍,这点请千万注意)。由此可见sizeof(struct A)的结果应该是=1+3(空闲空间)+4+2+2(结构体补充)=12字节。

接下来让我们考察sizeof(struct B)的结果。数据成员c的有效对齐值为1,存放起址0x00,s的有效对齐值为2,存放起址0x02,i的有效对齐值为4,存放起址为0x04,累加起来一共是8个字节,已经是数据结构B的有效对齐值4的整数倍了。因此sizeof(struct B)的结果8个字节。

对于联合体union:

#include <iostream>

#include <stdio.h>

using std::cout;

using std::endl;


union u1

{

double a;

int b;

};

union u2

{

char a[13];

int b;

}; 

union u3

{

char a[13];

char b;

}; 


#pragma pack(2)//对界方式2字节¨2

union u4

{

char a[13];

int b;

}; 


union u5

{

char a[13];

char b;

};

 

#pragma pack()  //恢复默认对界方式



void main()

{

cout<<sizeof(u1)<<endl;

cout<<sizeof(u2)<<endl;

cout<<sizeof(u3)<<endl;

cout<<sizeof(u4)<<endl;

cout<<sizeof(u5)<<endl;


system("pause");

}

测试结果如下:

8

16

13

14

13

Union就是取整个成员中大的内存块作为整个共用体的内存大小。

结论:由于默认是4字节(32位系统)对齐, 在u1 中a为8字节,很明显u1的大小就为8。在u2中由于int为4字节,char为1 个字节,所以min(4,max(4,1))=4,以4字节对齐,u2理论为13个字节,要以4字节对齐的话所以为16字节。同理 u3 :min(max(1,1),4)=1,sizeof(u3)=13, 

u4:min(max(1,4),2)=2,sizeof(u4)=14,

u5:min(max(1,1),2)=1,sizeof(u5)=13。







字节对齐问题详解