0%

C语言的内存对齐问题

1.什么是内存对齐?

以一个例子来说:

1
2
3
4
5
6
7
8
9
10
11
struct test{
char a;
int i;
char b;
};

int main(int argc, char **argv)
{
printf("size : %ld\n", sizeof(struct test));
return 0;
}

在 Inter i5 + Ubuntu 18.04 + gcc 7.5 上输出:

1
size : 12

并不是想象中的应该是 1 + 4 + 1 = 6 btyes,这正是由于内存对齐而产生的结果。

2. 为什么会进行内存对齐

原因1-可移植性

部分基于 arm 架构的 CPU 并不能随意访问任意地址上的数据,只能访问特定地址上的数据。在非内存对齐的情况下,以上面为例,假设成员 a 存放在地址0x0处,则成员 i 存放在 0x1 处,此时这些 CPU 执行二进制代码访问成员 i 时就会崩溃。因此,为了提高代码的可移植性,需要进行内存对齐。

原因2-提高读取速度

CPU 从内存地址中读取数据需要消耗时间。如果没有内存对齐,CPU 读取成员 i ,第一次读取地址0x0~0x3的数据,第二次读取地址0x4~0x7的数据,然后将地址0x1~0x4的数据进行拼接从而获取成员 i 的值,这样 CPU 获取成员 i 的值需要读取两次内存并进行一次拼接,再需要大量从内存中读取数据时会显著影响程序的运行效率。

3. C语言进行内存对齐的规则

一般来说,CPU按块读取内存,在 32 位系统中每块大小位 4 字节,在 64 位系统中是 8 字节。这个块大小称为对齐数,可以通过关键字 #pragma pack(N) 来手动指定对齐数,例如#pragma pack(4)

  1. 整体对齐数

    假设结构体成员中最大的类型占用的字节数为 m,则对齐单位 。结构体整体对齐后的字节数应为 的整数倍,不够应该在后面补占位字节。

  2. 成员对齐数

    假设首个成员的相对偏移地址为 0,某个成员的类型占用字节数为 j,则该成员的相对偏移地址应当满足 的整数倍

例如,在Inter i5 + Ubuntu 18.04 + gcc 7.5 的环境下:

1
2
3
4
5
6
7
8
// n = min(8,8) = 8
struct A{
char a; // offset = min(8,1) = 1,故位于地址0
double b; // offset = min(8,8) = 8,故位于地址8
int c; // offset = min(8,4) = 4,故位于地址16
char d; // offset = min(8,1) = 1,故位于地址20
};
// 整体应为8的整数倍,所以还需要补充3字节的占位

结构体所占用的大小为 24 字节。

当设置了 #pragma pack(4) 修改默认对齐数之后:

1
2
3
4
5
6
7
8
9
10
#pargma pack(4)

// n = min(8,4) = 4
struct A{
char a; // offset = min(4,1) = 1,故位于地址0
double b; // offset = min(4,8) = 4,故位于地址4
int c; // offset = min(4,4) = 4,故位于地址12
char d; // offset = min(4,1) = 1,故位于地址16
};
// 整体应为4的整数倍,所以还需要补充3字节的占位

结构体所占用的大小为 20 字节。

在嵌套结构体的情况下,应先对最内层的结构体进行内存对齐,并将对齐后所占用的字节数作为该类型的大小进行计算。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 占用8字节
struct B{
char a;
int i;
};

struct A{
char a;
double b;
int c;
char d;
struct B e; // offset = min(8,8) = 8,故位于地址24
};
// 整体占用32字节,刚好为8的整数倍,无需补位

结构体所占用的大小为 32 字节。

3.1 基于内存对齐规则减少内存浪费

让占用空间小的成员尽量集中在一起

例如上面的例子中,可以调整结构体成员的位置,使得该结构体所占用的内存空间最小:

1
2
3
4
5
6
struct A{
char a;
char d;
int c;
double b;
};

这样调整结构体成员的顺序之后,该结构体占用仅为 16 字节,比之前 24 字节还是减少了挺多的。

4. 位域

有时数据的存储并不需要一个完整的字节,仅需要几个二进制位就可以表示。在这种情况下,C语言支持位域存储,对于一个变量声明其仅需要几个二进制位来存储;使用位域能够节约存储占用。

例如:

1
2
3
4
5
6
7
struct pack
{
unsigned a:2; // 取值范围为:0~3
unsigned b:4; // 取值范围为:0~15
unsigned :0; // 匿名位域,用于占位
unsigned c:6; // 取值范围为:0~63
};

对于位域,如果赋值超过其取值范围,则参照无符号/有符号整型变量的溢出规则;

4.1 位域的存储

结构体中位域的存储遵循以下规则(对于 GCC 编译器):

  1. 对于 GCC 编译器会尽量利用空闲的位对数据进行存储;
  2. 若位域之间定义有匿名位域成员,则匿名位域成员指定的空闲位不用于后续成员的数据存储;
  3. 特别地,如果定义的结构体中匿名成员占用的位数为0,则该匿名成员占用该变量所剩下的所有位

4.2 位域的对齐规则

  1. 首先按照存储原则计算相邻的位域所占用的总字节数,该区域与 1 进行地址对齐
  2. 其他成员变量以及整体的对齐规则如第 3 节所示

4.2.1 例子

1
2
3
4
5
6
7
8
9
10
11
#pragma pack(4)

struct A{
char a; // offset = min(4,1) = 1,故位于地址0
double b; // offset = min(4,8) = 4,故位于地址4
int c; // offset = min(4,4) = 4,故位于地址12
char d; // offset = min(4,1) = 1,故位于地址16
char e:2;
unsigned int f:10; // 占用 char 的 6bit, 以及第一个字节的 4bit
unsigned int g:12; // 由于上一个unsigned int 仅使用 4bit,因此能够放在上一个 unsigned int 中,因此这 3 个位域总占用字节数为3
};

整个结构体刚好占用 20 个字节,和 4 对齐,因此不需要补字节。

1
2
3
4
5
6
7
8
9
10
11
struct A{
char a; // offset = min(4,1) = 1,故位于地址0
double b; // offset = min(4,8) = 4,故位于地址4
int c; // offset = min(4,4) = 4,故位于地址12
char d; // offset = min(4,1) = 1,故位于地址16
char e:2;
char :0; // 占位 6bit
unsigned int f:22; // 从地址18开始,占用4byte
unsigned int g:26; // 占用地址20的最后两个bit,以及地址21,22,23
// 因此整个位域占用7个字节
};

整个结构体刚好占用 24 个字节,和 4 对齐,因此不需要补字节。

5.取消内存对齐

虽然内存对齐可以加快读取内存的速度,但是在部分情况下需要取消默认的内存对齐,例如从 BMP 文件中读取 BMP 文件头时,需要让编译器取消内存对齐,防止其对结构体填充不必要的字节,使得结构体和文件中值不能刚好一一对应。

例1:__attribute__((packed))

1
2
3
4
5
6
7
8
9
typedef struct tagBITMAPFILEHEADER

{                               /* bmp文件头(bmp file header),一共14字节 */
    unsigned short bfType;      /* 文件类型,BMP的标志:0x42 0x4D */
    unsigned long bfSize;       /* 文件大小,以字节数表示 */
    unsigned short bfReserved1; /* 保留位,必须设为0 */
    unsigned short bfReserved2;
    unsigned long bfOffBits; /* 表示从文件头到位图数据的偏移,可能比较大,开头除文件头、信息头外可能还有调色板数据 */
} __attribute__((packed)) BITMAPFILEHEADER;

表示该结构体使用最小的内存,编译器不会对其做对齐;这里是读BMP文件的文件头,不能让编译器填充额外字节。

例2

1
2
3
4
5
struct example {
char a;
int b;
long c;
}__attribute__((aligned(4)));

指示该结构体按 4 字节对齐

参考:

C/C++内存对齐详解 - 知乎 (zhihu.com)

【C/C++】内存对齐 到底怎么回事? - 知乎 (zhihu.com)

c语言进阶部分详解(详细解析自定义类型——结构体,内存对齐,位段)-CSDN博客

C语言结构体位域及其存储-CSDN博客

位域与结构体的内存对齐_位域结构体对齐-CSDN博客