浅谈C++内存管理 —— 内存对齐

内存对齐

1、为什么要内存对齐

#include<iostream>
using namespace std;
struct A{
    char a;
    int b;
    short c;
};

struct B{
    short c;
    char a;
    int b;
};
int main(){
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    return 0;
}
程序的输出结果为:

 sizeof(st1) is 12
 sizeof(st2) is 8

问题出来了

1、这两个一样的结构体,为什么sizeof的时候大小不一样呢?

2、 编译器为什么要进行内存对齐呢?程序1中结构体按常理来理解sizeof(st1)和sizeof(st2)结果都应该是7,4(int) + 2(short) + 1(char) = 7 。经过内存对齐后,结构体的空间反而增大了。

第二节再说明。

关于数据的排放
在这里插入图片描述
这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。

在这里插入图片描述
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。

为什么要内存对齐?

尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,也就是上面所说的内存存取粒度.

现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。

假如没有内存对齐机制,数据可以任意存放。现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,

  • 1、要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),
  • 2、然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),
  • 3、最后留下的两块数据合并放入寄存器.这需要做很多工作.
    在这里插入图片描述
    现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
    在这里插入图片描述
    为什么要数据对齐?不对齐的话会产生什么样的结果?
  • 1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,一般可能以双字节、4字节等为单位存取内存,为了保证处理器正确存取数据,否则抛出硬件异常。(平台移植是驱动程序开发者经常需要考虑的问题)。
  • 2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。

2、什么是内存对齐

1、内存对齐

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

2、基本概念

1、每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。

2、有效对其值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。

3、内存对齐的规则:

  • 1、 第一个成员位于偏移为0的位置,变量的起始地址能够被其对齐值整除,结构体变量的对齐值为最宽的成员大小。
  • 2、以后每个数据成员的偏移量必须是min(#pragma pack()指定的数,这个数据成员的自身长度)的倍数。如果不能则在前一个成员后面补充字节。
  • 3、 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,也就是结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节。
    • 也就是,整体对齐,将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度

3、实例说明

linux下默认#pragma pack(4)

//32位系统
#include<stdio.h>
struct
{
    int i;    
    char c1;  
    char c2;  
}x1;

struct{
    char c1;  
    int i;    
    char c2;  
}x2;

struct{
    char c1;  
    char c2; 
    int i;    
}x3;

int main()
{
    printf("%d\n",sizeof(x1));  // 输出8
    printf("%d\n",sizeof(x2));  // 输出12
    printf("%d\n",sizeof(x3));  // 输出8
    return 0;
}

结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以x2结构体来分析其内存布局:

  • Step1 :使用规则1,char占一个字节,起始偏移为0 ,规则1的后半段在Step2说明。
  • Step2:使用规则2,int 占4个字节,min(#pragma pack()大小,int字节数) = 4
    • 基于规则1,所以int按4字节对齐起始偏移必须为4的倍数,所以起始偏移为4,在char后编译器会添加3个字节的额外字节,不存放任意数据。
    • 第二个char占1个字节,按1字节对齐,起始偏移为8,正好是1的倍数,无须添加额外字节。到此规则1的数据成员对齐结束,此时的内存状态为:
oxxx|oooo|o
0123 4567 8 (地址)

其中x为补上的字节
  • Step3:使用规则3,目前共占9个字节。还要继续进行结构本身的对齐。对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,选择较小的那个进行。
    • x2结构中最大数据成员长度为int,占4字节,而默认的#pragma pack 指定的值为4,所以结果本身按照4字节对齐,结构总大小必须为4的倍数,需添加3个额外字节使结构的总大小为12 。此时的内存状态为:
oxxx|oooo|oxxx

0123 4567 89ab  (地址)

到此内存对齐结束。x2结构占用了12个字节而非6个字节。

根据上面的分析,不难得出上面例子三个结构体的内存布局如下:
在这里插入图片描述

4、#pragma pack(n)

不同平台上编译器的 pragma pack 默认值不同。而我们可以通过预编译命令#pragma pack(n), n= 1,2,4,8,16来改变对齐系数。

例如,对于上个例子的三个结构体,如果前面加上#pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是6字节。

在这里插入图片描述
如果前面加上#pragma pack(2),有效对齐值为2字节,此时根据对齐规则,三个结构体的大小应为6,8,6。内存分布图如下:

在这里插入图片描述

参考

1、https://zhuanlan.zhihu.com/p/30007037

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值