关于内存对齐
《关于数据对齐小结》
以下内容均摘抄于网络资源。
一.关于数据的一些简介:
BSS段(bss segment):BSS段通常是指用来存放程序中未初始化,或初始化为0的全局变量,静态局部变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段(data segment):数据段通常是指用来存放程序中已初始化为非0的全局变量的一块内存区域。数据段属于静态内存分配。
代码段(code segment/text segment):代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
highest address
=========
| stack |
| vv |
| |
| |
| ^^ |
| heap |
=========
| bss |
=========
| data |
=========
| text |
=========
address 0
二.关于数据的排放
图一:
这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。
图二:
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。
假设CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:
<!--[if !supportLists]-->1、<!--[endif]-->数据从0字节开始
<!--[if !supportLists]-->2、<!--[endif]-->数据从1字节开始
再次假设内存读取粒度为4。
图三:
当该数据是从0字节开始时,很CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。
当该数据是从1字节开始时,问题变的有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。
图四:
此时CPU先访问一次内存,读取0—3字节的数据进寄存器,并再次读取4—5字节的数据进寄存器,接着把0字节和6,7,8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。
这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。
三.为什么要数据对齐?不对齐的话会产生什么样的结果?
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(平台移植是驱动程序开发者经常需要考虑的问题)。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(就是刚才所提到的对效率的影响)。
四.关于数据对齐细节问题:
1.字符串数组,起始地址不一定是四字节对齐的。对于ARM体系来说,如果访问的32位整数不是4字节对齐的,是会总线错误的(相对我们用的PC,不对齐仅仅是速度变慢)!
2.结构体或者联合体
原则1、数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。
原则2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
原则3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
很明显按照以上原则,分析之前T1,T2结构的存储方式如图所示,打X的是按照规则之后的补充位↓
五.先简要简要介绍一下ARM处理器是如何进行数据操作的:
ARM是32位处理器,armv4能高效的处理8,16,32位的数据,但是大多数arm处理器直接操作的是32位的数据。
地址跳变基数为4字节即4.一次存取数据量为32位。(硬件角度),我们一次取到的32位数据不一定是一个完整
的数据构,可能是两个数据结构,也可能是某个数据结构的一部分,(而编译器帮助我们将一条对数据结构操作
的C操作转化成多条对齐的汇编指令,这些从每次取到的32位数据中获得有用的值,合并重组而完成对一个数据结构的操作)
当对一个数据结构进行操作时,如边界不对齐,编译器可以将C操作转化成多条边界对齐的汇编操作,把结果合并、
重组来模拟对齐的操作(可见这种非对齐的存储是非常消耗效率的)。
软件角度,在软件方面我们定义的数据排列方式是由编译器决定的,根据编译器的对称规则进行数据排列,而常用的
数据操作指令(ARM指令中的)是以4字节为对称边界进行操作的。C中允许你干预“内存对齐”。
下面简要说明下内存对齐对高质量可移植代码的重要性:
在arm处理器中如果装载和存储的地址与数据类型的边界不对齐,那么可能产生异常的结果,例如:通常C编译器假定
指针是边界对齐的。如果指针不是边界对齐,那么程序执行会产生不正确的结果。这样,把代码从允许边界不对齐的
处理器移植到ARM处理器时就会出现问题。
六.最后再提一下对齐规则:
(1)对齐规则
1.__align(num)
这个用于修改最高级别对象的字节边界。在汇编中使用LDRD或者STRD时
就要用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。
这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节
对齐,但是不能让4字节的对象2字节对齐。
__align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。
2.__packed
__packed是进行一字节对齐
1.不能对packed的对象进行对齐
2.所有对象的读写访问都进行非对齐访问
3.float及包含float的结构联合及未用__packed的对象将不能字节对齐
4.__packed对局部整形变量无影响
5.强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定
义为packed。
(2)对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)
,n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据
成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma
pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果