1. 为什么需要内存对齐?

内存物理结构
我们来了解一下内存的物理构造,一般内存的外形图片如下图:
在这里插入图片描述

图1 内存外形图

一个内存是由若干个黑色的内存颗粒构成的。每一个内存颗粒叫做一个chip。每个chip内部,是由8个bank组成的。其构造如下图:
在这里插入图片描述

图2 chip内部构成

而每一个bank是一个二维平面上的矩阵,前面文章中我们说到过。矩阵中每一个元素中都是保存了1个字节,也就是8个bit。
在这里插入图片描述

图3 bank内部构成

内存编址方式
那么对于我们在应用程序中内存中地址连续的8个字节,例如0x0000-0x0007,是从位于bank上的呢?直观感觉,应该是在第一个bank上吗? 其实不是的,程序员视角看起来连续的地址0x0000-0x0007,实际上位8个bank中的,每一个bank只保存了一个字节。在物理上,他们并不连续。下图很好地阐述了实际情况。

在这里插入图片描述

图4 连续8字节在内存中实际分布

你可能想知道这是为什么,原因是电路工作效率。内存中的8个bank是可以并行工作的。 如果你想读取址0x0000-0x0007,每个bank工作一次,拼起来就是你要的数据,IO效率会比较高。但要存在一个bank里,那这个bank只能自己干活。只能串行进行读取,需要读8次,这样速度会慢很多。

结论
所以,内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个chip中各自读取一个字节拼起来的。从内存的0地址开始,0-7字节的数据可以一次IO读取出来,8-15字节的数据也可以一次读取出来。

换个例子,假如你指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回给你。 CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。这样你的应用程序就会变慢,算是计算机因为你不懂内存对齐而给你的一点点惩罚。

引入内存对齐的原因

  • 硬件取指的方便
  • 在于移植性的要求

2. 内存对齐的原则

原则1:数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员obj存储的起始位置要从该成员obj大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储,short是2字节,就要从2的整数倍开始存储)。

原则2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)

原则3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。如果是结构体B包含了结构体A对象a,判断最大成员时并不是a,而是a结构体的最大成员。
(补:上述取最大成员的大小后,实际上应该取[#pragma pack指定的数值]与[最大成员的数值]比较小的那个为准)

这三个原则具体怎样理解呢?我们看下面几个例子,通过实例来加深理解。
例1:

struct{
    short a1;
    short a2;
    short a3;
}A;
struct{
    long a1;
    short a2;
}B;

sizeof(A) = 6; 这个很好理解,三个short都为2。
sizeof(B) = 8; 这个比是不是比预想的大2个字节?long为4,short为2,整个为8,因为原则3。

例2:

struct A{
    int a;
    char b;
    short c;
};
struct B{
    char b;
    int a;
    short c;
};

sizeof(A) = 8; int为4,char为1,short为2,这里用到了原则1和原则3。
sizeof(B) = 12; 是否超出预想范围?char为1,int为4,short为2,怎么会是12?还是原则1和原则3。

深究一下,为什么是这样,我们可以看看内存里的布局情况。
A的内存布局:

a         b        c
1111,     1*,      11

B的内存布局:

 b         a        c
1***,     1111,    11**

其中星号*表示填充的字节。
A中,b后面为何要补充一个字节?因为c为short,其起始位置要为2的倍数,就是原则1。c的后面没有补充,因为b和c正好占用4个字节,整个A占用空间为4的倍数,也就是最大成员int类型的倍数,所以不用补充。

B中,b是char为1,b后面补充了3个字节,因为a是int为4,根据原则1,起始位置要为4的倍数,所以b后面要补充3个字节。c后面补充两个字节,根据原则3,整个B占用空间要为4的倍数,c后面不补充,整个B的空间为10,不符,所以要补充2个字节。

再看两个结构中含有结构成员的例子:
例3:

struct A{
    int a;
    double b;
    float c;
};
struct B{
    char e[2];
    int f;
    double g;
    short h;
    struct A i;
};

sizeof(A) = 24; 这个比较好理解,int为4,double为8,float为4,总长为8的倍数,补齐,所以整个A为24。
sizeof(B) = 48; 看看B的内存布局。

B的内存布局
e    f     g        h        i
11**,1111, 11111111,11******,1111****, 11111111, 1111****

例4:

struct A{
    int m1;
    int *m2;
}a;
struct B{
    int m1;
    struct A m2;
}b;
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(b));

输出16 24。
原则2,A的存储位置将由8开始,所以结构体B的成员b.m2.m1并不会与b.m1同存储于前八个字节中;
原则3,结构体的总大小并不是取决于结构体A的大小,而是A结构体的最大成员,所以总大小是24而不是32。

设计结构体习惯

最后顺便提一点,在设计结构体的时候,一般会遵照一个习惯,就是把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间。

全局关闭内存对齐

先来看下面的结构体:

struct test{
    int a;
    int b;
    char c;
}A;

1 添加预处理指令 #pragma pack(1)

#paragma pack(1)预处理指令的作用是结构体在分配内存时按一个字节对齐。

#include<stdio.h>
#pragma pack(1)
struct test{
    int a;
    int b;
    char c;
}A;
int  main()
{
    printf("sizeof(A) = %d\n",sizeof(A));
    return 0;
}

结果为:

$ ./a.out 
sizeof(A) = 9

实际上,如果用了#paragma pack(1)预处理指令,则整个文件中都会关闭内存对齐,如果只想对其中某个或某几个结构其关闭呢,这是就要用到第二种方法了。

选择性关闭内存对齐

利用__attribute__ ((packed))指令可以选择性的对内存对齐进行关闭。

#include<stdio.h>

#pragma pack(push)
#pragma pack(1)
struct testA{
    int a;
    int b;
    char c;
}A;
#pragma pack(pop)

struct testB{
    int a;
    int b;
    char c;
}B;

void main()
{
    printf("sizeof(A) = %d\n",sizeof(A));
    printf("sizeof(B) = %d\n",sizeof(B));
}
$ ./a.out 
9
12

参考文章:
1.https://zhuanlan.zhihu.com/p/83449008

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐