结构体内存对齐的意义
(1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。
(2)内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的;如果你不对齐访问效率要低很多。
(3)还有很多别的因素和原因,导致我们需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
(4)对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。
代码演示及运行结果分析
代码演示
#include <stdio.h>
#include <stdlib.h>
typedef struct
{ // 1字节对齐方式 4字节对齐方式
int a; // 4字节 4字节
char b; // 1字节 2字节+1填充字节
short c; // 2字节 1字节
}test1;
typedef struct
{ // 1字节对齐方式 4字节对齐方式
short c; //2字节 2字节+2填充字节
int a; //4字节 4字节
char b; //1字节 1字节+3填充字节
}test2;
typedef struct
{ // 1字节对齐方式 4字节对齐方式
short c; // 2字节 2字节+2填充字节
double d; // 8字节 8字节
char b; // 1字节 1字节+3填充字节
int a; // 4字节 4字节
}test3;
int main(void)
{
test1 s1;
test2 s2;
test3 s3;
printf("sizeof(s1)=%ld\n",sizeof(s1));
printf("sizeof(s2)=%ld\n",sizeof(s2));
printf("sizeof(s3)=%ld\n",sizeof(s3));
return 0;
}
运行结果展示
运行结果分析。
第一个:32位编译器,一般编译器默认对齐方式是4字节对齐。
总结下:结构体对齐的分析要点和关键:
1、结构体对齐要考虑:结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数(编译器设置为4字节对齐时,如果编译器设置为8字节对齐,则这里的4是8)
2、结构体中每个元素本身都必须对其存放,而每个元素本身都有自己的对齐规则。
3、编译器考虑结构体存放时,以满足以上2点要求的最少内存需要的排布来算。
test1结构体类型内存储分布,运行结果与我们的计算结果一致
test2结构体类型内存储分布,运行结果与我们的计算结果一致
test3结构体类型内存储分布,运行结果与我们的计算结果出现了差别,此时我们就要引出一个被我们遗漏的细节:
其实编译器在编译程序的时候不但对结构体内的成员进行了内存对齐操作,而且还对结构体整体进行了内存对齐操作。而结构体的对齐规则:结构体变量所占空间的大小是对齐参数的整数倍。这句话中的对齐参数就是指,取结构体中所有变量的对齐参数的最大值和系统默认对齐参数#pragma pack(n)在linux(32)系统中默认是8比较,较小者作为对齐参数。
由此可以得出test3结构体成员对齐后的大小是20字节但是test3结构体成员中最大对齐参数类型是double,对齐参数为8,而系统默认的 #pragma pack(n)为8,两者比较最小为8,所以结构体的大小必须为8的整数倍,因此还要在尾部填充4个字节使得结构体大小为24个字节满足8的整数倍。
test1与test2类型的结构体类似成员类型中最大对齐参数为int型的4字节,而系统默认的 #pragma pack(n)为8,两者比较最小值为4,因此结构体的大小要满足4的整数倍,正好test1和test2类型结构体成员对齐后就可以满足结构体对齐(4的整数倍的条件)因此结构体成员对齐后无需填充字节。
gcc对结构体对齐的支持
gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)
(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。
(2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。
(3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。
(4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。
gcc推荐的对齐指令__attribute__((packed)) attribute((aligned(n)))
(1)attribute((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。(即按照一字节对齐)
(2)attribute((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
一下代码是使用了__attribute__((packed))和__attribute__((aligned(n)))的代码演示和运行结果
typedef struct
{ // 1字节对齐方式 4字节对齐方式
int a; // 4字节 4字节
char b; // 1字节 2字节+1填充字节
short c; // 2字节 1字节
}__attribute__((aligned(16)))test1;
typedef struct
{ // 1字节对齐方式 4字节对齐方式
short c; //2字节 2字节+2填充字节
int a; //4字节 4字节
char b; //1字节 1字节+3填充字节
}__attribute__((packed))test2;
typedef struct
{ // 1字节对齐方式 4字节对齐方式
short c; // 2字节 2字节+2填充字节
double d; // 8字节 8字节
char b; // 1字节 1字节+3填充字节
int a; // 4字节 4字节
}__attribute__((aligned(16)))test3;
显然我们一眼就可以看出__attribute__((packed))是结构体test1不对齐。
而__attribute__((aligned(n)))使得结构体类型test2和结构体类型test3在成员进行4字节对齐后,在此大小的基础上通过满足n的整数倍的字节大小,如果不够n的整数倍,就填充字节到n的整数倍,以进行整个结构体的对齐。