字节对齐

 
这两天仔细研究了一下关于字节对齐(Alignment)的问题,现在写出来与大家分享与讨论,
欢迎指正。

1. 为什么要对齐?

以32位的CPU为例(16,64位同 ),它一次可以对一个32位的数进行运算,它的数据
总线的宽度是32位,它从内存中一次可以存取的最大数为32位,这个数叫CPU的字(word)长。

在进行硬件设计时,将存储体组织成32位宽,如每个存储体的宽度是8位,可用四块存储体
与CPU的32位数据总线相连(这也是为什么以前的 386/486 计算机插SIMM30内存条(8位)时,必
须同时插四条的原因),请参见下图:

1 8 16 24 32
-------- ------- ------- --------
| long1 | long1 | long1 | long1 |
-------- ------- ------- --------
| | | | long2 |
-------- ------- ------- --------
| long2 | long2 | long2 | |
-------- ------- ------- --------
| ....

当一个long型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU存取这个
数只需访问一次内存,而当一个long型数(如图中long2)在内存中的位置跨越字边界时,CPU存
取这个数就需多次访问内存,如 i960cx 访问这样的数需读内存三次(一个BYTE,一个short,
一个BYTE,由CPU的微代码执行,对软件透明),所以在对齐方式下,CPU的运行效率明显快多
了,这就是要对齐的原因。

一般在编译器生成代码时,都可以根据各种CPU类型,将变量进行对齐,包括结构(struct)
中的变量,变量与变量之间的空间叫padding,有时为了对齐在一个结构的最后也会填入padding,
通常叫tail padding。但在实际的应用中,我们确实有不对齐的要求,如在编通讯程序时,帧的结
构就不能对齐,否则会带来错误及麻烦。所以各编译器都提供了不对齐的选项,但由于这是ANSI C
中未规定的内容,所以各厂家的实现都不一样。下面是我们常用编译器的实现。

2. 一般编译器实现对齐的方法

由于各厂家的实现不一样,这里涉及的内容只使用于Visual C++ 4.x,Borland C++ 5.0、3.1
及pRism x86 1.8.7 (C languange),其他厂家可能略有不同。

每种基本数据类型都有它的自然对齐方式(Natural Alignment),Align的值与该数据类型的
大小相等,见下表:

Data Type sizeof Natural Align
(signed/unsigned)

char 1 1
short 2 2
long 4 4
.
.
.

同时用户还可以指定一个Align值(使用编译开关或使用#pragma),当用户指定一个Align值 n
(或编译器的缺省)时,每种数据类型的实际(当前)Align值定义如下:

Actual Align = min ( n, Natual Align ) //公式 1

如当用户指定Align值为 2 时,char 的实际Align值仍为 1,short及long的实际Align值为 2。
当用户指定Align值为 1 时,所有类型的实际Align值都为 1。

复杂数据类型(Complex or Aggregate type,包括 array, struct 及 union)的对齐值定义
如下:

struct:结构的Align值等于该结构所有成员的 Actual Align 值中最大的一个 Align 值,
注意成员的Align值是它的实际Align值。

array: 数组的Align值等于该数组成员的 Actual Align 值

union: 联合的Align值等于该联合最大成员的 Actual Align 值

同时当用户指定一个Align值时,上面的公式 1 同样起作用,只不过Natual Align应理解为
当前的Actual Align。

那么编译器是如何根据一个类型的Align值来分配存储空间(主要是在结构中的空间)的呢?
有如下两个规律:
1:一个结构成员的offset等于该成员Actual Align值的整数倍,如果凑不成整数倍,就
在其前加padding
2:一个结构的大小等于该结构Actual Align值的整数倍,如果凑不成整数倍,就在其后
加padding(tail padding)。一个结构的大小在其定义时就已确定,不会因为其
Actual Align值的改变而改变。

例如有如下两个结构定义:

#pragma pack(8) //指定Align为 8
struct STest1
{
char ch1;
long lo1;
char ch2;
} test1;
#pragma pack()

现在 Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )
test1在内存中的排列如下( FF 为 padding ):
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 01 01 01 01 FF FF FF
ch1 -- lo1 -- ch2

#pragma pack(2) //指定Align为 2
struct STest2
{
char ch3;
STest1 test;
} test2;
#pragma pack()

现在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )
test2在内存中的排列如下:
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF
ch3 ch1 -- lo1 -- ch2

从以上可以看出,用户可以在任何需要的地方定义不同的align值。

3. 不同编译器实现用户指定align值的方法

因为是 ANSI C 中未规定的内容,所以各厂家的方法都不一样,一般都提供命令行选项及使
用#pragma。命令行选项对所有被编译的文件都起作用。#pragma则是ANSI C特别为实现不同
的编译器及平台特性而规定的预处理器指令(Preprocessor)。下面主要讲一下#pragma的
实现。

Visual C++ :VC使用 #pragma pack( [n] ),其中 n 可以是 1, 2, 4, 8, 16, 编译器在
遇到一个#pragma pack(n)后就将 n 当作当前的用户指定aling值,直到另一个#pragma
pack(n),当遇到一个不带 n 的 pack 时,就恢复以前使用的align值。

Borland C++:BC使用 #pragma option -an ,在 BC 5.0 的Online Help中没有发现对
#pragma pack的支持,但发现在其系统头文件中使用的都是#pragma pack。

pRism x86 : 使用 #pragma pack( [n] ) ,但奇怪的是 C 文件与 C++ 文件生成的代码不一
样,有待进一步研究。

gcc960 : 使用 #pragma pack n 及 #pragma align n,两个开关的意义不一样,并且相互
作用,比较复杂,但同时使用 #pragma pack 1 及 #pragma align 1 可以实现与
Visual C++中 #pragma pack(1) 一样的功能。

其他编译器的方法各不相同,可参见手册。如果要使用不同的编译器编译软件时,就要针对不
同的编译器使用不同的预处理器指令。

4. 使用 #pragma pack (或其他开关)需注意的问题

1. 为了保证执行速度,尽量不使用#pragma pack。

2. 不同的编译器生成的代码极有可能不同,一定要查看相应手册,并做实验。

3. 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如果很多人
使用该头文件,并不是每个人都知道应该pack。特别是为别人开发库文件时,如果一个库函
数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,
而且该类错误很不好查。在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,
都加了pack,否则我们编的Windows程序哪一个也不会正常运行。

4. 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值,
将产生非预期结果。
VC中提供了一种安全使用pack的方法:
#pragma pack( [ push | pop ], n )
#pragma pack( push, n )将当前的align值压入编译器的一个内部堆栈,并使用 n 作为当
前的align值,而#pragma pack(pop)则将内部堆栈中的栈顶值作为当前的align值,这样就
保证了嵌套pack时的正确。

5. 不要多人同时定义一个数据结构。在多人合作开发一个软件模块时,为了保持自己的编程
风格,每个人都要对同一结构定义一份符合自己风格的数据类型,当两个人之间需要传递
该数据结构时,如果两个人的 pack 值不一样,就会产生错误,该类错误也很难查。所以,
为了安全起见,我们还是舍弃一些自己的风格吧。


5. 关于位域( Bit Field )

在 ANSI C 中规定位域的类型只能为 signed/unsigned int,但各厂家都对其进行了扩展,类
型可以是 char, short, long 等,但其最大长度不能超过int的长度,即32位平台时为32位,
16位平台时为16位。位域存储空间的分配也与各编译器的实现有关,而且与Little Endian(x86,
i960),Big Endian(680x0,PowerPc)有关,所以在定义位域时要对不同的编译器进行不同的支持。

如在VC中规定,如果两个连续位域的类型不一样,或位域的长度为零,编译器将进行对齐。

在VC中是这样,其他编译器就可能不是这样,这属于各厂家不同的实现问题,ANSI C 中没有进
行规定,所以如果涉及到位域问题,一定要查看手册。

6. 附例

以下结果均在VC++4.x,BC++5.0,3.1,pRism x86 1.8.7(C Language) 进行过验证。其中因为
BC++ 3.1 是16位的,所以只有pack(1),pack(2)有效。

例中定义了如下几个结构:

typedef struct tagSLong
{
char chMem1;
char chMem2;
char chMem3;
unsigned short wMem4;
unsigned long dwMem5;
unsigned short wMem6;
char chMem7;
}SLong;

typedef struct tagSShort
{
char chMem1;
unsigned short wMem2;
char chMem3;
}SShort;

typedef union tagun
{
char uChar;
unsigned short uWord;
}un;

typedef struct tagComplex
{
char chItem1;
SLong struItem2;
unsigned long dwItem3;
char chItem4;
un unItem5;
}Complex;

你可能感兴趣的:(字节对齐)