甚至和程序是否能正常运转都有很大关系。
在这里我将先简要说明一下内存对齐的基础知识。这些内容来自于IBM developerworks。
内存访问粒度 memory access granularity
程序员可以先将内存简单的认为是一个一维数组。在C和衍生语言里,char*代表着这样一块
内存,而在Java中甚至会用到byte[]来表示内存块。
程序员眼中的内存
我们可以看到内存从0到最大值顺序排列,每一个内存单元都被一个数字索引,这就是内存
地址。
但是现代的处理器却往往不是以一个比特为单位来访问内存的,而是以2,4,8,16,甚至
32个字节为单位。我们把这个尺寸称之为内存访问粒度,用来表示处理器一次存取的内存大
小。
处理器眼中的内存
内存对齐基础知识
我们用固定比特读取这样一个例子,来分析内存对齐。
首先是访问粒度为1比特的处理器
这和程序员脑子中的内存模型是一致的,每次读取一个比特。那么从地址0读取和地址1读
取4个字节,情形完全相同。
来看看访问粒度为2的情况
从0读取的时候,粒度为2的处理器将会花费1/2的时间来读取同样大小的内存(与粒度1相
比)。读取4个字节,将只需要2次存取。
然而,地址1的读取则大不相同,因为地址1没有落在处理器的内存边界上,处理器的活儿就
多了。这样的地址被成为“不对齐地址”。由于地址1没有对齐,那么粒度为2的处理器将会多
花费1个存取次数,显然降低了访问速度。
最后我们来查看粒度为4的处理器
从边界地址上读取4个字节,这里只访问了一次内存。但是从“不对齐地址”处读取,依然会使
读取次数加倍。
懒惰的处理器
回想到我们上面所陈述的3种情况,我们来总结一下非对齐地址的读取。
首先处理器需要从非对齐地址读取第一个内存块,然后将不需要的内存位移出内存块,然后
再从非对齐地址读到第二个内存块,同样移除不需要的内存地带。最后将两者合并到一起,
放置到寄存器里。OH,这真是一个麻烦的任务~
而且……有些处理器不愿意为你作这样的工作。
原先的68000就是粒度为2的处理器,他却缺乏处理非对齐地址的相应电路,因此当遇到此类
访问的时候,处理器会抛出异常。早期的Mac OS对待这种异常非常不友好,很可能会要求用
户重启计算机。听起来实在无奈~
现在所有的处理器都会使用有限数量的晶体管来完成此类任务,在这类问题上会更上一层楼。
但无论怎样,非对齐地址的内存访问始终大大落后于对齐地址,这里处理器必须面对的一个
现实。
编译器中的数据对齐
struct P{
char a;
long b;
char c;
};
struct P的尺寸会是多少?很多程序员会不假思索的回答:6(1+4+1)。他们假想的元素布局
可能会是这样:
Field Type | Field Name | Field Offset | Field End | Field Size |
char | a | 0 | 0 | 1 |
long | b | 1 | 4 | 4 |
char | c | 5 | 5 | 1 |
Total Size in Bytes: | 6 |
然而,问问你的编译器就很知道,sizeof(struct P)是12 (GCC4)。真实的结构体布局是:
Field Type | Field Name | Field Offset | Field End | Field Size |
char | a | 0 | 0 | 1 |
long | b | 4 | 7 | 4 |
char | c | 8 | 8 | 1 |
Total Size in Bytes: | 12 |
这和我们刚才所说的内存对齐紧密相连。为了保持较高的内存访问效率,编译器也会在数据
结构存储上应用到数据对齐。对齐原则针对平台,编译器,可能会有较大不同,这也是这篇
文章的重点。
首先引入对齐模数概念,编译器会要求数据结构的成员地址是某个数k的倍数,这个常数k则
被称为该数据类型的对齐模数 (alignment modulus,余下简称modulus)。它是成员地址的
公约数,也是成员偏移量的公倍数。
结构体对齐原则
(下面的对齐原则,仅仅针对于GCC)
struct A {
char a;
char b;
};
//sizeof(struct A) = 2;
struct B {
short a;
short b;
short c;
};
//sizeof(struct B) = 6;
struct C {
int a;
int b;
int c;
};
//sizeof(struct C) = 12;
这样看很简单。sizeof似乎没有什么问题。char,short,int的长度分别是1,2,4,这样可以
轻而易举的加到结果。
struct D {
short a;
int b;
unsigned c;
};
//sizeof(struct D) = 12;
这里先确定对齐模数,针对VC和GCC编译器,他们各有不同。
cl.exe:
选取成员中的最宽类型的字长为modulus(对齐模数)
GCC:
同样选取成员中字长最大值,但是对齐模数只在三个值间选择,他们是1,2,4。
这意味着GCC结构体的最大modulus只能是4。
struct D中最长类型为int,那么它在GCC中的对齐modulus是sizeof(int) = 4;
同样,这里阐述结构体对齐的三条原则:
1) 结构体变量的首地址必须能够被modulus所整除
2)* 结构体每个成员相对于首地址的偏移量(Offset)必须能被modulus整除,如有需要,在
成员间填充空白字节(这被称为internal padding)
struct D的成员 a 使用到了internal padding。
3) 结构体总大小必须是modulus的整数倍,如有需要,在最末元素填充字节(trailing
padding)
成员C就用到了 trailing padding。
根据以上原则我们可以列出struct D内存布局图:
Field Type | Field Name | Field Offset | Field End | Field Size |
short | a | 0 | 1 | 2 |
/ | (padding) | 2 | 3 | 2 |
int | b | 4 | 7 | 4 |
char | c | 8 | 8 | 1 |
/ | (padding) | 9 | 11 | 3 |
Total Size in Bytes: | 12 |
struct E { // offset/data size
short a; // 0/2,空位填充
int b; // 4/4
char c; // 8/1,邻接元素填充
short d; // 10/2
};
//sizeof(struct E) = 12;
内存布局:
Field Type | Field Name | Field Offset | Field End | Field Size |
short | a | 0 | 1 | 2 |
/ | (padding) | 2 | 3 | 2 |
int | b | 4 | 7 | 4 |
char | c | 8 | 8 | 1 |
/ | (padding) | 9 | 9 | 1 |
short | d | 10 | 11 | 2 |
Total Size in Bytes: | 12 |
这里引用到了GCC的压缩存储。压缩存储要求,结构体的成员会紧凑的将成员压缩到一个
modulus里。GCC的强压缩方式,又可以忽略元素类型,将不同类型的成员压缩到一个
modulus里。
然后阐述的原则将会对对齐原则2重新定义:
4) 考虑到GCC的压缩存储方式,邻近成员会合并到一个modulus里,合并过程直到可能超出
modulus大小时停止
5) 合并在一个modulus里的数个成员,将会按照“子结构体”的方式来存储。比如拥有更小的
mini modulus,按照mini modulus排列数据成员
struct E 中的元素c,d就使用到了压缩存储。那么c和d将会按照“子结构体”方式存储,
他们拥有的modulus是2 (sizeof(short))。
/* modulus只能考虑基本数据类型,因此结构体成员还需追溯其成员类型 */
struct F {
char a;
struct A b; //
};
//sizeof(struct F) = 3;
对齐模数只能考虑原子数据类型,因此:
6) 成员结构体需要打破结构体边界,追溯成员类型。数组也是一样,modulus将和数组大小
无关
union和class对齐
除了struct以外,C中包含有union,而在C++中更是包含了人见人爱的class支持。他们同
struct一样,是属于符合类型,允许定义多个不同类型的成员。
我们来看看union的特性:
union G {
char a;
int b;
char c[10];
};
通过实验,我们看到结果:sizeof(union F) = 12;
* union仍旧选用最宽数据类型作为modulus(和struct一样)
union G 的modulus是4 (int)。
* 而union采用完全完全压缩方式,它占有的总内存大小,将和最大成员字长一样。
union G的最大成员是char c[10],再考虑modulus=4,因此取总字长为12。
然后我们写出union G的内存布局。
Field Type | Field Name | Field Offset | Field End | Field Size |
char | a | 0 | 0 | 1 |
int | b | 0 | 3 | 4 |
char | c | 0 | 11 | 12 |
Total Size in Bytes: | 12 |
class H {
private:
int a;
static char b; // static 不占空间
short c;
unsigned short d;
void func0(); // public func不占空间
public:
int e; // 公私有对sizeof无影响
virtual void func1(); // 一个类的virtual只能占据一个指针
void func2();
virtual void func3(); //
virtual void func4();
virtual void func5(); //
};
//sizeof(H) = 16;
class概念是C++的根基,作为C的超集,C++在class定义中添加了非常多的特性。我们简单
说一下class对齐和内存占用原则:
1) modulus大小和原子类型排列的原则和struct相同
class H将仍旧选取4(int)为对齐modulus。
2) 内存占用和访问控制修饰符无关(比如private,class,protected)
成员a, e各自享用4个字节。
3) static 变量和成员函数,在class结构中不占用空间
static char b将不占内存空间。
4) 如果类包含虚函数,那么要算上一个指针的空间(虚指针)
所有的虚函数(virtual)都共享一个虚指针,原class字长+4
5) 子类将使用基类的modulus。由于继承了成员,子类也将会包含基类的所有字节大小,在
此基础上定义的新成员则按照上述3个法则计算
引用:
1. wikipedia wiki: sizeof
2. Rentzsch, Jonathan. "Data alignment: Straighten up and fly right."
3. king. "sizeof(结构体)和内存对齐"
Just a comment test here.
回复删除