内存对齐模式 —— 原理讲解

内存对齐模式定义:
当前变量的首地址,与当前变量的首地址有关,即:若当前类型的类型是type,那么,当前变量的首地址必须是sizeof(type)的整数倍
例如:定义这样一个结构体:
typedef struct NEW_TYPE{
int one;
char two;
short three;
double four;
};
假设这个结构体第一个变量从编号为0的空间开始存储,那么,该结构体所占空间如下图所示:
在这里插入图片描述

那么,为什么计算机存储数据要使用内存对齐模式呢?
首先,为了与之后所讲内存管理模式形成对比,我先提及一下非页式管理:
非页式管理的原理是连续存储空间的分配与回收。
而连续存储空间的分配与回收,会造成大量的内存碎片,这种碎片被称为“外部碎片”;而在碎片过多的情况下,会出现:所有可用空间之和满足要求,但是,任何一个空闲碎片都不满足要求而导致内存分配失败的现象。
为了解决这个现象,可以进行“压缩”,即:将所有已分配空间移动在一起,将所有可用空间“合并”成一个大的可用空间,但,这种操作非常消耗系统资源,同时,要涉及复杂的“重定位”技术。

总的来看,上述问题的根本原因是:内存的连续存储分配。
而我们如果能够挣脱这个约束,那么,上述问题就能够解决了。

为了解决上述问题,这里提出“页式分配方案”:

1.将物理内存分成大小相同的若干“页”,这成为“物理页面”,并对这些“页”进行编号;
2.将软件按物理页面大小进行分割,分割成若干“逻辑页面”,从0开始连续地对这些逻辑页面开始编号;
3.按照逻辑页面的顺序,对于每一个逻辑页面,在物理页面中找“尚未使用的”页面,并分配给这些逻辑页面;
4.分配的同时,生成由逻辑页号和物理页号作为数值对数值的集合,这个集合称为“页面表”。
说明:地址映射过程如下:
逻辑编号/物理页面大小(找相应的逻辑页号)
按照页面表找到相应的物理页好
逻辑编号%物理页面大小(找到在相应物理页面中的偏移量)
上述的地址映射过程要进行两次除法,这是十分耗时的!
为了加快这样的运算,可以及那个物理页面大小设置为2^n
(因为:m/(2^n) <=> m>>n; m & (2^n); m % (2^n) <=> m & ((2^n) -1 ),这样的话,就完全转化为位运算了,能大大降低计算时间)

那么,页式分配方案和内存对齐模式又有什么关系呢?
可以这样假设,假如没有内存对齐模式,那么,数据将是连续存储,那么,根据页式分配方案,可能会出现一个数据被分成了两半,分别放在两个物理页面中,这样的话,读取一个数据就要进行两次内存访问,十分耗时,所以,内存对齐模式将极大地提高效率。

那么,如果我们想要改变内存对齐模式的默认长度怎么办呢?
下面引进 #pragma命令:

pragma 命令是向编译器提供额外信息的标准方法,使用pragma 命令可能会导致代码可移植性降低,但是,最新版本的 GNU C 编译器和微软 Visual C 编译器都支持 #pragma pack(n),这个命令使得编译器让结构成员对齐到特定的字节边界(即n),默认情况下,n取4。

而pack(1)指示每个结构成员必须对齐到字节边界,及连续存储,这样能够避免出现上面所提及的内存碎片。

下面,我们来编写一段代码,来了解一下#pragma命令的使用方法吧:

#include <stdio.h>

#pragma pack(push)
#pragma pack(1)

typedef struct {
	int one;
	char two;
	int three;
	double four;
}NEW_TYPE;

int main() {
	NEW_TYPE num = {
		0x12,
		'A',        // 0x41
		0x135,
		3.14,
	};
	FILE *fp;

	fp = fopen("abcde.dat", "wb");
	fwrite(&num, sizeof(MY_TYPE), 1, fp);

	fclose(fp);

	return 0;
}

#pragma pack(pop)

这里的#pragma pack(push)是将系统默认的对其模式保留下来,而#pragma pack(pop)是将自己定义的对其模式删去,避免代码对计算机的修改导致有些功能无法运行。

有关#pragma 命令的使用方法详解,请参考如下文章(这位博主对于#pragma命令的讲解比较透彻):
《C语言#pragma使用方法》

posted @ 2020-03-04 17:53  在下右转,有何贵干  阅读(467)  评论(0编辑  收藏  举报