C语言精要总结-内存地址对齐与struct大小判断篇
在笔试时,经常会遇到结构体大小的问题,实际就是在考内存地址对齐。在实际开发中,如果一个结构体会在内存中高频地分配创建,那么掌握内存地址对齐规则,通过简单地自定义对齐方式,或者调整结构体成员的顺序,可以有效地减少内存使用。另外,一些不用边界对齐、可以在任何地址(包括奇数地址)引用任何数据类型的的机器,不在本文讨论范围之内。
什么是地址对齐
计算机读取或者写入存储器地址时,一般以字(因系统而异,32位系统为4个字节)大小(N)的块来执行操作。数据对齐就是将数据存储区的首地址对齐字大小(N)的某个整数倍地址。为了对齐数据,有时需要在物理上相邻的两个数据之间保留或者插入一些无意义的字节。内存对齐本事编译器考虑是事情,但在C、C++语言中,可以人为修改对齐方式。
为什么要地址对齐
计算机会保证存储器字的大小,至少要大于等于计算机支持的最大原始数据类型的大小。
这样,一个原始数据类型就一定可以存放在一个存储器字中,如果保证了数据是地址对齐的,那么访问一个原始数据就可以保证只访问一个存储器字,这有利于提高效率。如下图
反之,如果一个数据不是按字大小内存对齐的(也就是最高字节与最低字节落在两个字中),那么,这个数据很可能落在两个存储器字中。如下图
这时,计算机必须将数据访问分割成多个存储器字访问,这需要更多复杂的操作。甚至,当这两个字都不存在一个存储器页中是,处理器还必须在执行指令之前验证两个页面是否存在,否则可能会发生未命中错误。另外,对一个存储器字的操作是原子的,如果拆分成两次访问,也可能引发一些并发问题,比如从两个字读出来的数据段拼起来可能不是真实的数据,因为有另外的设备在写。
起始地址约束(对齐系数)
C++11 引入 alignof 运算符,该运算符返回指定类型的对齐系数(以字节为单位),其中宏__alignof在linux gcc或者windows都有定义。
下面一段程序取几个常用的基本数据类型。
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main(){ 4 printf("char: %d\n",__alignof(char)); 5 printf("short: %d\n",__alignof(short)); 6 printf("int: %d\n",__alignof(int)); 7 printf("long: %d\n",__alignof(long)); 8 printf("double: %d\n",__alignof(double)); 9 return 0; 10 }
分别在linux和windows下编译运行,得到如下结果
类型 | Linux | Windows |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 8 | 4 |
double | 8 | 8 |
可以看到Linux下与Windows下,long类型对齐系数不一样。并且对齐系数与类型自身所占的大小也基本一致。
地址对齐对struct大小的影响
地址对齐主要影响到一些复杂的数据结构,比如struct结构体,因为有了内存地址对齐,大多数的struct实际占用的大小显得有些诡异。(注意,一个结构体的大小很可能超过存储器字大小,这时跨字读取数据已不可避免。但结构体本身及其成员还是需要继续遵守对齐规则)
拿一个很简单的结构体align1为例
1 struct align1 2 { 3 char a; 4 int b; 5 char c; 6 } sim[2];
如果不考虑任何对齐问题,只考虑结构体中每个成员应该占用的大小,很显然每个结构align1定义的变量是1(char)+4(int)+1(char)共6个字节。但是实际上(至少在windows上)它占用了12个字节,原因就在于它有按照一定的规则进行内存地址对齐。下面是笔者参考各方面资料总结的四点结构体边界对齐需满足的要点:
- 结构体变量本身的起始位置,必须是结构成员中对边界要求最严格(对齐系数最大)的数据类型所要求的位置
- 比如double类型的起始地址约束(对齐系数)为8 ,那如果一个结构体包含double类型,则结构体变量本身的起始地址要能被8整除
- 成员必须考虑起始地址约束(对齐系数)和本身的大小,在windows和linux下,都可以使用__alignof(type)来查看type类型(原始基本类型)的起始地址约束(对齐系数)。
- 如果成员也是struct union之类的类型,则整体要照顾到部分,整体要满足成员能符合起始地址约束
- 结构体可能需要在其所有成员之后填充一些字节,以保证在分配结构体数组之后,每个数组元素要满足起始地址约束。
让我们再来仔细研究下结构体 align1定义的实例数组 sim[2]。我们先约定:占用即表示本身大小及其后的空余空间。
按要点1,则sim[0]的起始地址必须能被4整除,假设这个其实地址是4n,其中成员a的起始地址也是sim[0]的起始地址(按要点2,因为a 为char类型,对齐系数为1,放哪都可以),a占用一个字节。
按要点2,成员b的起始地址必须能被4整除,很显然不能直接放在成员a的后面(起始地址是4n+1,不能被4整除),所以需要跳过3个字节存放b,那么成员a实际占用了4个字节(我们的约定)。
同理,成员c可以直接放在b成员后面(起始地址是(4(n+2)),而且肯定可以被1整除)。
至此,sim[0]已经占用了9个字节了,但按照要点4,因为数组是连续的,为了保证其后的数组成员sim[1]也符合首地址能被4整除,必须将sim[0]的空间先后延长3个字节至(4(n+3))。所以sim[0]实际要占用12个字节。
当然一个结构体不能有两个大小,哪怕其后不再放align1类型的变量,系统也要为这个变量分配最大的12个字节空间。
用一个简单的占位符来表示存储,可表示为
1 // --sim[0]---- ----sim[1]-- 2 // a---bbbbc--- a---bbbbc---
用图片描述如图(一个正方形表示一个字节空间)
很显然,这个结构体对空间利用率不高,有50%的空间浪费。通过调整成员定义的顺序,完全可以优化空间利用。个人的经验是,本身占用空间大的(如double类型)应该尽量往前面放。下面我们将int b;调整到第一位定义
1 struct align2 2 { 3 int b; 4 char a; 5 char c; 6 } sim[2];
通过分析不难发现,新的结构占用8个字节的空间。如图
空间利用率提高到75%。当一个结构体足够复杂时,通过调整顺序或者自定义对齐方式,压缩带来的空间是非常可观的。虽然,随着内存越做越大,一般情况下开发已经不需要考虑这种问题。但是在海量服务下,如何死抠性能和减少资源占用依然是开发需要考虑的问题。就像现在单机几十万并发tcp连接已经不难做到,为什么还是有很多人在研究C10M(单机千万连接)。
下面的程序是基于以上四项要点做的测试,特别注意MyStruct7,因为其中的成员包含数组。至于成员包含union的就比较简单了,一般可以直接把union用union中最大的成员替换考虑,另外注意考虑要点3。另外,在一个位段定义中使用非int 、signed int 、或者unsigned int类型,位段定义将变成一个普通的结构体,对齐原则也就遵从结构体的对齐原则。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <stddef.h> 4 /************************************************************************/ 5 /* 这里约定:占用即表示本身大小及其后的空余空间 6 /************************************************************************/ 7 struct MyStruct1 // 起始地址能被8整除 8 { 9 char a; // 8 10 double b; // 8 11 float c; // 4 12 int d; // 4 13 } m1; // 24 14 15 struct MyStruct2 // 起始地址能被8整除 16 { 17 int a; // 4 18 float b; // 4 19 char c; // 8 // 后面 double的起始地址要能被8 整除,所以c补齐8个字节 20 double d; // 8 21 } m2; // 24 22 23 struct MyStruct3 // 起始地址能被8整除 24 { 25 short a; // 2 26 char b; // 6 // 同理,后面的元素的起始地址要能被 8 整除,所以b只要占用6 27 double c; // 8 28 int d; // 8 // 需要在其后填充一些字节,以保证在分配数组之后,每个数组元素要满足起始地址约束 29 } m3; // 24 30 31 struct MyStruct4 32 { 33 char a; // 2 // 能被4整除的地址 +2之后能被2整除,所以a只要补1个字节 34 short b; // 2 35 int c; // 4 36 } m4; // 8 37 38 struct MyStruct5 // 起始地址能被8整除 39 { 40 double a; // 8 41 float b; // 4 42 int c; // 4 43 short d; // 2 44 char e; // 6 因为后面紧紧挨着的MyStruct5 变量(在分配数组的时候)起始地址也要能被8整除,所以这个结构体总的大小必须是8的整数倍 45 } m5; // 24 46 47 struct MyStruct6 // 除4对齐 48 { 49 short a; // 2 50 char b; // 2 51 long c; // 4 52 short d; // 4 // 保证数组后面的元素也符合规则 (结构体首地址可以除4) 53 } m6; // 12 54 55 struct MyStruct7 // 4 对齐 56 { 57 int a; // 4 58 char b; // 2 59 short c; // 2 60 char d[6]; // 8 61 } m7; // 16 62 63 int main(){ 64 printf("m1 size : %d\n",sizeof m1); 65 printf("m2 size : %d\n",sizeof m2); 66 printf("m3 size : %d\n",sizeof m3); 67 printf("m4 size : %d\n",sizeof m4); 68 printf("m5 size : %d\n",sizeof m5); 69 printf("m6 size : %d\n",sizeof m6); 70 printf("m7 size : %d\n",sizeof m7); 71 72 // offsetof 函数用来计算成员离结构体首地址偏移的字节数 73 printf("MyStruct1 b offset : %d\n",offsetof(struct MyStruct1,b)); // b偏移8个字节,所以成员a占用8个字节 74 printf("MyStruct2 d offset : %d\n",offsetof(struct MyStruct2,d)); // d偏移了16个字节 75 printf("MyStruct3 c offset : %d\n",offsetof(struct MyStruct3,c)); // 偏移8 76 printf("MyStruct4 b offset : %d\n",offsetof(struct MyStruct4,b)); // 偏移2 77 printf("MyStruct5 e offset : %d\n",offsetof(struct MyStruct5,e)); // 偏移16 78 printf("MyStruct6 c offset : %d\n",offsetof(struct MyStruct6,c)); // 偏移4 79 printf("MyStruct7 c offset : %d\n",offsetof(struct MyStruct7,c)); // 偏移 80 system("pause"); 81 return 0; 82 }
测试代码
文中所用的windows为windows7 64位, gcc版本为:gcc version 5.1.0 (tdm64-1);linux为CentOSLinux release 7.2.1511 (Core),gcc版本是gcc version 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC)
参考文章:https://en.wikipedia.org/wiki/Data_structure_alignment
转载于:https://www.cnblogs.com/lvyahui/p/6785284.html
C语言精要总结-内存地址对齐与struct大小判断篇相关推荐
- c语言地址和地址怎么比较大小,C语言精要总结-内存地址对齐与struct大小判断篇...
在笔试时,常常会遇到结构体大小的问题,实际就是在考内存地址对齐.在实际开发中,若是一个结构体会在内存中高频地分配建立,那么掌握内存地址对齐规则,经过简单地自定义对齐方式,或者调整结构体成员的顺序,能够 ...
- 内存地址对齐提升程序性能
内存地址对齐,是一种在计算机内存中排列数据.访问数据的一种方式,包含了两种相互独立又相互关联的部分:基本数据对齐和结构体数据对齐.当今的计算机在计算机内存中读写数据时都是按字(word)大小块来进行操 ...
- 内存地址对齐问题研究所分析
1.指针对齐问题 CPU一般要求指针的值(内存地址)要与它的指向类型数据的尺寸相匹配.例如,2个字节的数据类型被访问的地址值为 2 的倍数,4个字节的数据类型(如 int)被访问的地址值是 4 的倍数 ...
- c语言字符指针清零,C语言中字符串的内存地址操作的相关函数简介
C语言bcopy()函数:复制内存(字符串)头文件: #include bcopy() 函数用来复制内存(字符串),其原型为: void bcopy(const void *src, void *de ...
- c语言20字节的内存的数据怎么读取_C++编程-内存对齐
内存对齐可以大大提升内存访问速度,是一种用空间换时间的方法. 1.内存对齐的计算机原理 内存地址对齐,是一种在计算机内存中排列数据(表现为变量的地址).访问数据(表现为CPU读取数据)的一种方式,包含 ...
- 嵌入式开发中数值常量如何转化为内存地址?
作者 | 李肖遥 责编 | 欧阳姝黎 最近在使用 Nordic 的最新蓝牙芯片 nRF52832 开发过程中,因为做一些测试涉及到对内存地址的操作,有(*(volatile unsigne ...
- 关于AVALON总线动态地址对齐
在NIOS的使用中,我们往往要用到自定义外设,然后通过AVALON交换架构和NIOSII进行通信. AVALON总线,其实是一种交换架构的协议,在自定义外设挂在AVALON总线上时,一定要注意地址对齐 ...
- C语言 泛型链表 如何计算(结构体中各元素)相对内存地址?(字节对齐,结构体对齐)offsetof()函数 ( (struct X*)0 ) -> Y)语法(匿名结构体)
示例: typedef struct _user {char name[20];char sex[20];int age;struct list_head mylist;//自定义结构体里保存双向循环 ...
- 【嵌入式】C语言高级编程-地址对齐(07)
00. 目录 文章目录 00. 目录 01. 数据对齐概述 02. 数据需要对齐的原因 03. 属性声明: aligned 04. 结构体对齐 05. 附录 01. 数据对齐概述 一般情况下,当我们定 ...
最新文章
- centos下pg_dump的服务器版本不匹配问题
- Xampp修改数据库默认用户root的密码
- Java蓝桥杯——贪心算法
- [轉]SQL Server 2005备份维护计划
- 主流html5桌面应用开发,主流HTML5开发工具推荐
- 在linux查看内存的大小
- Spark的StaticMemoryManager
- 在pycharm中使用matplotlib时需要点❌才能显示下一张图片的问题
- bulk interface驱动_USB驱动程序(较详细)三
- Axure 9母版引发事件
- 关于PCIe有gen1升到gen3的方法
- 电脑位数(32位或者64位)问题导致eclipse不能正常启动
- Something about ...
- 计算机游戏设计师要学什么软件,从事游戏设计工作需要学什么专业
- ps cs6安装问题汇总
- unity3d 角色 武器 动画 和 blender 工作流
- STM32中断优先级分组概念
- Netty 实现一对一客户端聊天(由服务器转发)
- 阿里和云之讯短信发送服务
- 企业在知乎上做问答推广的技巧分析,企业知乎推广营销方法步骤
热门文章
- supersocket缓冲区_关于supersocker的数据传输中遇到的问题
- 计算机图形学多边形填充代码_计算机图形学 Computer Graphics (第一周笔记及课件翻译)...
- oracle 物料属性批次过期,物料批次特性值
- mysql jdbc官方,mysql_jdbc
- 2012.4.19总结(一)
- linux 中rpc 服务器,实现Linux环境下编程RPC通信之个人经验总结(转)
- 将数所有奇数移到数组前java_全国2014年4月自考Java语言程序设计(一)真题
- linux ip命令dhcp,嵌入式linux通过DHCP自动获取IP地址实现获取
- 依赖反转原则(DIP)
- idea查找类 方法在哪里被使用