为什么需要字对齐
-
硬件原因
现代计算机中内存空间是按照
byte
划分的。从理论上讲:对任何类型的变量的访问都可以从任何地址开始。
但实际情况是:计算机并非逐个字节读写内存,而是以 一定的字节数 来读写内存。
如此一来就会对基本数据类型的合法地址作出一些限制。那么就要求各种数据类型按照一定的规则在空间上排列,这就是 对齐 1。
而当某类型的变量所在的地址恰好为其他类型长度的整数倍,则称为 自然对齐。
某些硬件平台只能从某些地址处取某些特定类型的数据,例如否则抛出 硬件异常 2。
-
性能原因
字节对齐可以 提高CPU访问数据的效率,举个栗子:
存在一整型变量(假设其大小为
4byte
),存储的地址为0x00000001
。在以 4byte 为最小读取单位的机器上,读取该变量需要两次,第一次读取
0x00000000
-0x00000003
,第二次读取0x00000004
-0x00000008
。若该变量存储地址为
0x00000004
,则只需读取一次:0x00000004
-0x00000008
。
对齐原则
标准数据类型
前面举了一个整型(标准数据类型)的例子,可以发现其地址只要是其存储大小的整数倍即是对齐的。
示例 3
int main(){
char a = 'a';
int b = 1;
int* c = &b;
double d = 3.14159265;
int e = 2;
printf("Addr:%p,sizeof(a):%lu \n",&a,sizeof(a));
printf("Addr:%p,sizeof(b):%lu \n",&b,sizeof(b));
printf("Addr:%p,sizeof(c):%lu \n",&c,sizeof(c));
printf("Addr:%p,sizeof(d):%lu \n",&d,sizeof(d));
printf("Addr:%p,sizeof(e):%lu \n",&e,sizeof(e));
system("PAUSE");
return 0;
}
运行结果
图示
发现
-
其地址均为基本数据类型大小的整数倍;
-
内存地址并不是紧密挨着啊;
-
栈上各变量申请的内存,返回的地址是这段连续内存的最小的地址。
ps:本人系统为
64bit Windows10
;开发环境是VSCode
;编译链是gcc version 8.1.0
。
至于gcc
的默认对齐,依赖于ABI
。绝非广为流传的 4字节对齐 4。不信各位可以自己试试。
非标准数据类型
那么对于非标准数据类型,诸如结构体、数组,又是如何对齐的呢?
-
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
-
联合 :按其包含的长度最大的数据类型对齐。
-
结构体:结构体中每个数据类型都要对齐 5。
ps:
自身对齐值 =max
{各成员变量对齐值}
指定对齐值:#pragma pack (value)
指定对齐值value。
有效对齐值 =min
{自身对齐值,当前指定的pack值} 6
结构体
示例
int main(){
struct Test_Aligned
{
char a;
int b;
int c;
};
printf("sizeof(f):%lu\n",sizeof(struct Test_Aligned));
struct Test_Aligned a;
printf("a.a addr = %p\n",&a.a);
printf("a.b addr = %p\n",&a.b);
printf("a.d addr = %p\n",&a.c);
system("PAUSE");
return 0;
}
结果及图示
可以发现,结构体中的每个变量仍然符合标准数据类型的规律, 其地址均为成员变量大小的整数倍 。且结构体中的变量在内存中是连续存储的。
当我们把结构体改为
struct Test_Aligned
{
char a;
int b;
short c;
};
其结果为:
再修改为以下结构体:
struct Test_Aligned
{
char a;
int b;
char c;
};
其结果为:
多次实验得:当结构体总大小非 有效对齐值 的整数倍时,将向后扩充以满足该要求。
例如 :
struct Test_Aligned
{
char a;
int b;
short c;
};
有效对齐值为 4
;因此最后会填充 2byte
,使结构体总大小为 12
,满足被 4
整除的条件 。
再例:
struct Test_Aligned
{
char a;
double b;
short c;
};
结果:
小结
-
结构体变量的首地址能够被自身对齐值所整除。
-
结构体中每个成员相对结构体首地址的偏移都是 成员变量对齐值 的整数倍,如不满足,对前一个成员填充字节以满足。
-
结构体的总大小为 有效对齐值 的整数倍,如不满足,最后填充字节以满足 1。
联合
union Data
{
int i;
double x;
char str[16];
};
由于联合中所有成员均从内存中同一地址开始,因此该联合大小为成员中最大存储大小。
位域
有时候我们仅需要1bit
来存储信息(例如开关变量),为了节省存储空间,C语言又提供了一种数据结构,把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数,允许在程序中按域名进行操作。常称之为“位域”或“位段”。
需要注意的是:位段实现依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
//定义
struct 位域结构名
{
类型说明符位域名:位域长度
//ps:位域名可为空,若如此,则该位域变量仅起填充作用
//...
};
//使用
位域变量名.位域名
位域大小
如下代码为例:
struct D
{
unsigned int a:20;
unsigned int b:22;
unsigned int c:2;
}data;
unsigned int
所占空间为 4byte
即32bit
,位域变量a占20bit
,接下来b占22bit
,4byte
不够分了,于是在下一 unsigned int
上分配,分配完剩下10bit
,其中2bit
分配给c。最终总占用大小为8byte
。
如果是以下位域:
struct D
{
unsigned int a:1;
long long b:1;
unsigned int c:2;
}data;
则先申请4byte
,分配给a 1bit
;再申请16byte
,分配给b 1bit
,再申请4byte
,分配给c。
所以,最终占用的大小为24byte
。
为了安全操作,通常会将空余的位进行填充。
例如这样:
struct D
{
unsigned int a:20;
unsigned int :12;
unsigned int b:22;
unsigned int c:2;
unsigned int :8;
}data;
亦可使用 unsigned int :0;
令下一位域成员与下一个unsigned int
类型对齐。
对齐规则 6
C99
规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型的存在。位域作为嵌入式系统中非常常见的一种编程工具,优点在于压缩程序的存储空间。
其对齐规则大致为:
-
如果相邻位域字段的类型相同,且其位宽之和小于类型的
sizeof()
大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止; -
如果相邻位域字段的类型相同,但其位宽之和大于类型的
sizeof()
大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍; -
如果相邻的位域字段的类型不同,则各编译器的具体实现有差异;
-
如果位域字段之间穿插着非位域字段,则不进行压缩;
使用场景
位域的使用主要为下面两种情况 6:
-
当机器可用内存空间较少而使用位域可大量节省内存时。如把结构作为大数组的元素时。
-
当需要把一结构体或联合映射成某预定的组织结构时。如需要访问字节内的特定位时。
如何修改默认对齐
-
__attribute__
__attribute__机制是
GCC
的一大特色,可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。详细介绍请点击 参考 6。-
__attribute__ ((aligned (number)))
让所作用的结构体成员对齐在 number 字节自然边界,若结构体体中有成员变量长度大于 number ,则按照最大成员变量长度对齐。
-
__attribute__((packed))
得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(
field
)是位对齐。亦可理解为取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐 7。
使用示例:
struct Test_Aligned { char a; int b; short c; }__attribute__((packed));
-
-
#pragma pack()
-
#pragma pack(number)
默认对齐大小修改为 number。
-
#pragma pack()
同
__attribute__((packed))
。
使用示例:
#pragma pack(8) struct Test_Aligned { char a; int b; short c; }; #pragma pack()
-
编程考量
实际上,字节对齐的细节都由编译器来完成,我们不需要特意进行字节的对齐,但并不意味着我们不需要关注字节对齐的问题 1。
默认对齐
Linux gcc
没有固定的默认字节对齐,当前推测默认对齐为 自身对齐值。
struct test
{
char a;
long double b;
short c;
};
struct test A;
printf("%d",sizeof(A));
//结果为:48
其他的系统要求的对齐可能不相同,依赖与其使用的 ABI
4,因此我们应尽量自己指定默认对齐,以防踩坑。
这里贴上博主 Gerald Kwok 咨询相关问题后得到的官方回函:
存储优化
通过调整结构体的成员变量位置,以减小结构体存储空间。
例如:
//12byte
struct Test_Aligned
{
char a;
int b;
short c;
};
//8byte
struct Optimization_Aligned
{
char a;
short c;
int b;
};
跨平台问题
在不同平台上,编译器为了使字节对齐可能会对消息结构体进行填充,而不同编译平台可能填充为不同的形式,从而使结构体大小发送改变,大大增加处理器间数据通信的风险。
解决方法:
- 将对齐格式设置为
1byte
对齐 - 空置内容填充
此外,跨平台传输还需注意字节序问题(大段存储/小端存储)。
附件
C常见数据类型大小
. | 指针 | char | short | int | unsigned int | float | double | long int | long long int |
---|---|---|---|---|---|---|---|---|---|
32bit | 4 | 1 | 2 | 4 | 4 | 4 | 8 | 4 | 16 |
64bit | 8 | 1 | 2 | 4 | 4 | 4 | 8 | 8 | 16 |