本文转自C语言字节对齐问题详解, 做了一定的删减和修改。
1. 什么是字节对齐
现代计算机中内存空间都是按照字节(byte)划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序地一个接一个地排放,这就是对齐。
2. 对齐的原因和作用
不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。
但最常见的情况是,如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。
因此,通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。
此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。
3. 对齐的分类和准则
本文主要基于Intel X86架构介绍结构体对齐,栈内存对齐和位域对齐,位域本质上为结构体类型。
对于Intel X86平台,每次分配内存应该是从4的整数倍地址开始分配,无论是对结构体变量还是简单类型的变量。
3.1 结构体对齐
在C语言中,结构体是种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合等)的数据单元。编译器为结构体的每个成员按照其自然边界(alignment)分配空间。各成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
字节对齐的问题主要就是针对结构体。
3.1.1 简单示例
先看个简单的例子(32位,X86处理器,GCC编译器):
【例1】设结构体如下定义:
1 | struct A{ |
已知32位机器上各数据类型的长度为:char为1字节、short为2字节、int为4字节、long为4字节、float为4字节、double为8字节。那么上面两个结构体大小如何呢?
结果是:sizeof(strcut A)值为8;sizeof(struct B)的值却是12。
结构体A中包含一个4字节的int数据,一个1字节char数据和一个2字节short数据;B也一样。按理说A和B大小应该都是7字节。之所以出现上述结果,就是因为编译器要对数据成员在空间上进行对齐。
3.1.2 对齐准则
先来看四个重要的基本概念:
- 数据类型自身的对齐值:就是基本数据类型的自身对齐值,比如char类型的自身对齐值为1字节,int类型的自身对齐值为4字节。
- 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值,比如以上的struct A的对齐值为4。
- 指定对齐值:预编译命令#pragma pack (value)指定的对齐值value。
- 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
其中,有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N表示“对齐在N上”,即该数据的“存放起始地址%N=0”。而数据结构中的数据变量都是按定义的先后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体本身也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。
基于上面定义和说明,我们可以方便的得出结构体字节对齐满足的三个一般性准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
- 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
以此分析3.1.1节中的结构体B:
假定对齐值默认为4,则char的有效对齐值为1,int的有效对齐值为4,short的有效对齐值为2;则结构体的有效对齐值为4;假设结构体B内存起始地址为0x0000, 按照结构体B的变量定义顺序,可以简单地知道变量char b内存地址为0x0000,占1个字节;变量int a 内存地址为0x0004,占4个字节;变量short c内存地址为0x0008,占2个字节。至此结构体的大小为(0x0008-0x0000) + 2 = 10; 根据结构体圆整的要求(准则3),编译器会在后面补充2个字节以满足4的整数倍要求,所以sizeof(struct B)=12。
再来看一个例子:
【例2】假设4字节对齐,以下程序的输出结果是多少?
1 | /* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */ |
执行后输出如下:
1 | Size = 16 |
下面来具体分析:
首先char a占用1个字节,没问题。
short b本身占用2个字节,根据上面准则2,需要在b和a之间填充1个字节。
char c占用1个字节,没问题。
int d本身占用4个字节,根据准则2,需要在d和c之间填充3个字节。
char e[3];本身占用3个字节,根据原则3,需要在其后补充1个字节。
因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字节。
3.1.3 对齐的隐患
3.1.3.1 数据类型转换
代码中关于对齐的隐患,很多是隐式的。例如,在强制类型转换的时候:
1 | int main(void){ |
最后两句代码,从奇数边界去访问unsigned short型变量,显然不符合对齐的规定。在X86上,类似的操作只会影响效率;但在MIPS或者SPARC上可能导致error,因为它们要求必须字节对齐。
3.1.3.2 排查对齐问题
如果出现对齐或者赋值问题可查看:
- 编译器的字节序大小端设置;
- 处理器架构本身是否支持非对齐访问;
- 如果支持看设置对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。
3.1.4 更改对齐方式
主要是更改C编译器的缺省字节对齐方式。
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
- 使用伪指令
#pragma pack(n)
:C编译器将按照n个字节对齐; - 使用伪指令
#pragma pack()
: 取消自定义字节对齐方式。
另外,还有如下的一种方式(GCC特有语法):
__attribute((aligned (n)))
: 让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。__attribute__ ((packed))
: 取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
【注】__attribute__
机制是GCC的一大特色,可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
3.2 栈内存对齐
待整理。
3.3 位域对齐
待整理。