内存对齐与补齐

2024-02-11 21:18
文章标签 内存 对齐 补齐

本文主要是介绍内存对齐与补齐,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

首先我们先看看下面的C语言的结构体:

[cpp] view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. typedef struct MemAlign  
  2. {  
  3.     int a;  
  4.     char b[3];  
  5.     int c;  
  6. }MemAlign;  
typedef struct MemAlign
{
int a;
char b[3];
int c;
}MemAlign;

    以上这个结构体占用内存多少空间呢?也许你会说,这个简单,计算每个类型的大小,将它们相加就行了,以32为平台为例,int类型占4字节,char占用1字节,所以:4 + 3 + 4 = 11,那么这个结构体一共占用11字节空间。好吧,那么我们就用实践来证明是否正确,我们用sizeof运算符来求出这个结构体占用内存空间大小,sizeof(MemAlign),出乎意料的是,结果居然为12?看来我们错了?当然不是,而是这个结构体被优化了,这个优化有个另外一个名字叫“对齐”,那么这个对齐到底做了什么样的优化呢,听我慢慢解释,再解释之前我们先看一个图,图如下:

    相信学过汇编的朋友都很熟悉这张图,这张图就是CPU与内存如何进行数据交换的模型,其中,左边蓝色的方框是CPU,右边绿色的方框是内存,内存上面的0~3是内存地址。这里我们这张图是以32位CPU作为代表,我们都知道,32位CPU是以双字(DWORD)为单位进行数据传输的,也正因为这点,造成了另外一个问题,那么这个问题是什么呢?这个问题就是,既然32位CPU以双字进行数据传输,那么,如果我们的数据只有8位或16位数据的时候,是不是CPU就按照我们数据的位数来进行数据传输呢?其答案是否定的,如果这样会使得CPU硬件变的更复杂,所以32位CPU传输数据无论是8位或16位都是以双字进行数据传输。那么也罢,8位或16位一样可以传输,但是,事情并非像我们想象的那么简单,比如,一个int类型4字节的数据如果放在上图内存地址1开始的位置,那么这个数据占用的内存地址为1~4,那么这个数据就被分为了2个部分,一个部分在地址0~3中,另外一部分在地址4~7中,又由于32位CPU以双字进行传输,所以,CPU会分2次进行读取,一次先读取地址0~3中内容,再一次读取地址4~7中数据,最后CPU提取并组合出正确的int类型数据,舍弃掉无关数据。那么反过来,如果我们把这个int类型4字节的数据放在上图从地址0开始的位置会怎样呢?读到这里,也许你明白了,CPU只要进行一次读取就可以得到这个int类型数据了。没错,就是这样,这次CPU只用了一个周期就得到了数据,由此可见,对内存数据的摆放是多么重要啊,摆放正确位置可以减少CPU的使用资源。

那么,内存对齐有哪些原则呢?我总结了一下大致分为三条:
第一条:第一个成员的首地址为0
第二条:每个成员的首地址是自身大小的整数倍
       第二条补充:以4字节对齐为例,如果自身大小大于4字节,都以4字节整数倍为基准对齐。
第三条:最后以结构总体对齐。
        第三条补充:以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。(其中这一条还有个名字叫:“补齐”,补齐的目的就是多个结构变量挨着摆放的时候也满足对齐的要求。)

    上述的三原则听起来还是比较抽象,那么接下来我们通过一个例子来加深对内存对齐概念的理解,下面是一个结构体,我们动手算出下面结构体一共占用多少内存?假设我们以32位平台并且以4字节对齐方式:

[cpp] view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. #pragma pack(4)   
  2. typedef struct MemAlign  
  3. {  
  4.     char a[18];  
  5.     double b;     
  6.     char c;  
  7.     int d;    
  8.     short e;      
  9. }MemAlign;  
#pragma pack(4)
typedef struct MemAlign
{
char a[18];
double b;	
char c;
int d;	
short e;	
}MemAlign;

下图为对齐后结构如下:

我们就以这个图来讲解是如何对齐的:
第一个成员(char a[18]):首先,假设我们把它放到内存开始地址为0的位置,由于第一个成员占18个字节,所以第一个成员占用内存地址范围为0~18。
第二个成员(double b):由于double类型占8字节,又因为8字节大于4字节,所以就以4字节对齐为基准。由于第一个成员结束地址为18,那么地址18并不是4的整数倍,我们需要再加2个字节,也就是从地址20开始摆放第二个成员。
第三个成员(char c):由于char类型占1字节,任意地址是1字节的整数倍,所以我们就直接将其摆放到紧接第二个成员之后即可。
第四个成员(int d):由于int类型占4字节,但是地址29并不是4的整数倍,所以我们需要再加3个字节,也就是从地址32开始摆放这个成员。
第五个成员(short e):由于short类型占2字节,地址36正好是2的整数倍,这样我们就可以直接摆放,无需填充字节,紧跟其后即可。
    这样我们内存对齐就完成了。但是离成功还差那么一步,那是什么呢?对,是对整个结构体补齐,接下来我们就补齐整个结构体。那么,先让我们回顾一下补齐的原则:“以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。”在这个结构体中最大类型为double类型(占8字节),又由于8字节大于4字 节,所以我们还是以4字节补齐为基准,整个结构体结束地址为38,而地址38并不是4的整数倍,所以我们还需要加额外2个字节来填充结构体,如下图红色的就是补齐出来的空间:

到此为止,我们内存对齐与补齐就完毕了!接下来我们用实验来证明真理,程序如下:

[cpp] view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. #include <stdio.h>   
  2. #include <memory.h>   
  3.   
  4. // 由于VS2010默认是8字节对齐,我们   
  5. // 通过预编译来通知编译器我们以4字节对齐   
  6. #pragma pack(4)   
  7.   
  8. // 用于测试的结构体   
  9. typedef struct MemAlign  
  10. {  
  11.     char a[18]; // 18 bytes   
  12.     double b;   // 08 bytes    
  13.     char c;     // 01 bytes   
  14.     int d;      // 04 bytes   
  15.     short e;    // 02 bytes   
  16. }MemAlign;  
  17.   
  18. int main()  
  19. {  
  20.     // 定义一个结构体变量   
  21.     MemAlign m;  
  22.     // 定义个以指向结构体指针   
  23.     MemAlign *p = &m;  
  24.     // 依次对各个成员进行填充,这样我们可以   
  25.     // 动态观察内存变化情况   
  26.     memset( &m.a, 0x11, sizeof(m.a) );  
  27.     memset( &m.b, 0x22, sizeof(m.b) );  
  28.     memset( &m.c, 0x33, sizeof(m.c) );  
  29.     memset( &m.d, 0x44, sizeof(m.d) );  
  30.     memset( &m.e, 0x55, sizeof(m.e) );  
  31.     // 由于有补齐原因,所以我们需要对整个   
  32.     // 结构体进行填充,补齐对齐剩下的字节   
  33.     // 以便我们可以观察到变化   
  34.     memset( &m, 0x66, sizeof(m) );  
  35.     // 输出结构体大小   
  36.     printf( "sizeof(MemAlign) = %d"sizeof(m) );  
  37. }  
#include <stdio.h>
#include <memory.h>
// 由于VS2010默认是8字节对齐,我们
// 通过预编译来通知编译器我们以4字节对齐
#pragma pack(4)
// 用于测试的结构体
typedef struct MemAlign
{
char a[18];	// 18 bytes
double b;	// 08 bytes	
char c;		// 01 bytes
int d;		// 04 bytes
short e;	// 02 bytes
}MemAlign;
int main()
{
// 定义一个结构体变量
MemAlign m;
// 定义个以指向结构体指针
MemAlign *p = &m;
// 依次对各个成员进行填充,这样我们可以
// 动态观察内存变化情况
memset( &m.a, 0x11, sizeof(m.a) );
memset( &m.b, 0x22, sizeof(m.b) );
memset( &m.c, 0x33, sizeof(m.c) );
memset( &m.d, 0x44, sizeof(m.d) );
memset( &m.e, 0x55, sizeof(m.e) );
// 由于有补齐原因,所以我们需要对整个
// 结构体进行填充,补齐对齐剩下的字节
// 以便我们可以观察到变化
memset( &m, 0x66, sizeof(m) );
// 输出结构体大小
printf( "sizeof(MemAlign) = %d", sizeof(m) );
}

程序运行过程中,查看内存如下:

其中,各种颜色带下划线的代表各个成员变量,蓝色方框的代表为内存对齐时候填补的多余字节,由于这里看不到补齐效果,我们接下来看下图,下图篮框包围的字节就是与上图的交集以外的部分就是补齐所填充的字节。

在最后,我在谈一谈关于补齐的作用,补齐其实就是为了让这个结构体定义的数组变量时候,数组内部,也同样满足内存对齐的要求,为了更好的理解这点,我做了一个跟本例子相对照的图:


 

 

来源:http://blog.csdn.net/cyousui/article/details/17655051

这篇关于内存对齐与补齐的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/700827

相关文章

在Spring Boot中浅尝内存泄漏的实战记录

《在SpringBoot中浅尝内存泄漏的实战记录》本文给大家分享在SpringBoot中浅尝内存泄漏的实战记录,结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录使用静态集合持有对象引用,阻止GC回收关键点:可执行代码:验证:1,运行程序(启动时添加JVM参数限制堆大小):2,访问 htt

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

java字符串数字补齐位数详解

《java字符串数字补齐位数详解》:本文主要介绍java字符串数字补齐位数,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java字符串数字补齐位数一、使用String.format()方法二、Apache Commons Lang库方法三、Java 11+的St

Redis 内存淘汰策略深度解析(最新推荐)

《Redis内存淘汰策略深度解析(最新推荐)》本文详细探讨了Redis的内存淘汰策略、实现原理、适用场景及最佳实践,介绍了八种内存淘汰策略,包括noeviction、LRU、LFU、TTL、Rand... 目录一、 内存淘汰策略概述二、内存淘汰策略详解2.1 ​noeviction(不淘汰)​2.2 ​LR

Python中如何控制小数点精度与对齐方式

《Python中如何控制小数点精度与对齐方式》在Python编程中,数据输出格式化是一个常见的需求,尤其是在涉及到小数点精度和对齐方式时,下面小编就来为大家介绍一下如何在Python中实现这些功能吧... 目录一、控制小数点精度1. 使用 round() 函数2. 使用字符串格式化二、控制对齐方式1. 使用

Golang基于内存的键值存储缓存库go-cache

《Golang基于内存的键值存储缓存库go-cache》go-cache是一个内存中的key:valuestore/cache库,适用于单机应用程序,本文主要介绍了Golang基于内存的键值存储缓存库... 目录文档安装方法示例1示例2使用注意点优点缺点go-cache 和 Redis 缓存对比1)功能特性

Go使用pprof进行CPU,内存和阻塞情况分析

《Go使用pprof进行CPU,内存和阻塞情况分析》Go语言提供了强大的pprof工具,用于分析CPU、内存、Goroutine阻塞等性能问题,帮助开发者优化程序,提高运行效率,下面我们就来深入了解下... 目录1. pprof 介绍2. 快速上手:启用 pprof3. CPU Profiling:分析 C

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Linux内存泄露的原因排查和解决方案(内存管理方法)

《Linux内存泄露的原因排查和解决方案(内存管理方法)》文章主要介绍了运维团队在Linux处理LB服务内存暴涨、内存报警问题的过程,从发现问题、排查原因到制定解决方案,并从中学习了Linux内存管理... 目录一、问题二、排查过程三、解决方案四、内存管理方法1)linux内存寻址2)Linux分页机制3)

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在