【系统运维】内存对齐(二)

给你举个例子,如下结构体成员的大小是a+b=1+4=5个字节,但是事实上结构体的大小是8,因为要字节对齐,就是说32位的机器 
//它的每个地址是4个字节,那如果有零头也会按4个字节算,这样方便取地址,程序如下: 
#include <stdio.h> 
struct A{ 
char a; 
int b; 
};

int main (){ 
A ab; 
printf("sizeof(A)=%d,sizoeof(a)=%d,sizoe(b)=%d/n",sizeof(ab),sizeof(ab.a),sizeof(ab.b)); 
}

 

在没有程序中自定义对齐方式的情况下编译器一般默认对齐方式为4字节对齐,那么此结构体大小是 
12字节 
不是9字节,是因为为了内存对齐。 
之所以有内存对齐问题,我个人认为是为了提高程序执行效率。即牺牲空间节省时间。 
在这里,具体内存分配情况如下: 
struct header 

BYTE by; //占用1字节 
DWORD dw; //为了内存对齐他从第5个字节开始存储,并占用4个 
int flag; //从第9个字节开始存储,并占用4个 
}; 
如果想强制只使用类型所占空间的内存,那么使用如下语句 
#pragma pack(1)

 

sizeof(结构体)和内存对齐 
Oct 4th, 2007 by king

有的时候,在脑海中停顿了很久的“显而易见”的东西,其实根本上就是错误的。就拿下面的问题来看:

struct T 

char ch; 
int i ; 
}; 
使用sizeof(T),将得到什么样的答案呢?要是以前,想都不用想,在32位机中,int是4个字节,char是1个字节,所以T一共是5个字节。实践出真知,在VC6中测试了下,答案确实8个字节。哎,反正受伤的总是我,我已经有点麻木了,还是老老实实的接受吧!为什么答案和自己想象的有出入呢?这里将引入内存对齐这个概念。

许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。

ANSI C标准中并没有规定,相邻声明的变量在内存中一定要相邻。为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。对于基本数据类型(int char),他们占用的内存空间在一个确定硬件系统下有个确定的值,所以,接下来我们只是考虑结构体成员内存分配情况。

Win32平台下的微软C编译器(cl.exe for 80×86)的对齐策略: 
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除; 
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。 
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding); 
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。 
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。 
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

根据以上准则,在windows下,使用VC编译器,sizeof(T)的大小为8个字节。

而在GNU GCC编译器中,遵循的准则有些区别,对齐模数不是像上面所述的那样,根据最宽的基本数据类型来定。在GCC中,对齐模数的准则是:对齐模数最大只能是4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是1,2,4。而且在上述的三条中,第2条里,offset必须是成员大小的整数倍,如果这个成员大小小于等于4则按照上述准则进行,但是如果大于4了,则结构体每个成员相对于结构体首地址的偏移量(offset)只能按照是4的整数倍来进行判断是否添加填充。 
看如下例子:

struct T 

char ch; 
double d ; 
}; 
那么在GCC下,sizeof(T)应该等于12个字节。

如果结构体中含有位域(bit-field),那么VC中准则又要有所更改: 
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止; 
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍; 
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式; 
备注:当两字段类型不一样的时候,对于不压缩方式,例如:

struct N 

char c:2; 
int i:4; 
}; 
依然要满足不含位域结构体内存对齐准则第2条,i成员相对于结构体首地址的偏移应该是4的整数倍,所以c成员后要填充3个字节,然后再开辟4个字节的空间作为int型,其中4位用来存放i,所以上面结构体在VC中所占空间为8个字节;而对于采用压缩方式的编译器来说,遵循不含位域结构体内存对齐准则第2条,不同的是,如果填充的3个字节能容纳后面成员的位,则压缩到填充字节中,不能容纳,则要单独开辟空间,所以上面结构体N在GCC或者Dev-C++中所占空间应该是4个字节。

4) 如果位域字段之间穿插着非位域字段,则不进行压缩; 
备注: 
结构体

typedef struct 

char c:2; 
double i; 
int c2:4; 
}N3; 
在GCC下占据的空间为16字节,在VC下占据的空间应该是24个字节。 
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

ps:

对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。而对于对齐准则中的第2条,确是要将整个结构体看成是一个成员,成员大小按照该结构体根据对齐准则判断所得的大小。 
类对象在内存中存放的方式和结构体类似,这里就不再说明。需要指出的是,类对象的大小只是包括类中非静态成员变量所占的空间,如果有虚函数,那么再另外增加一个指针所占的空间即可。

 


关于内存对齐问题.
悬赏分:0 - 解决时间:2007-7-30 06:02
没弄明白内存对齐的方式. 
#pragma pack(2) 
struct test_t { 
int a;/* 长度4 > 2 按2对齐;起始offset=0 0%2=0;存放位置区间[0,3] */ 
char b;/* 长度1 < 2 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */ 
short c;/* 长度2 = 2 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */ 
char d;/* 长度1 < 2 按1对齐;起始offset=8 8%1=0;存放位置区间[8] */ 
}; 
#pragma pack() 
为什么c要占用6,7单元而不是5,6单元呢.
问题补充:那是怎么保证第一个元素的内存地址都是在0位置的呢?
提问者: wm09323 - 魔法师 四级 最佳答案
pack(2) 就是要保证变量在2字节边界对齐,即每个变量的起始地址是2的倍数 
b存放在区间[4] ,区间〔5〕闲置不用

以此类推了,pack(4)就是4字节对齐。。。。

 

 

看错, 
#pragma pack(n)的作用你还没理解透

这个n的值可以为1, 2, 4, 8, 16,pack的原则是尽量使其在内存中和机器的自然字长相同,你的机器自然字长可能是2字也就是4字节,所以当n > 4时,字节数小于4的类型会被自动填充为4字节,大于4字节的才根据n的指定来填充,所以你那个类的内存排列方式应该是这样:

vptr: 4, int 4, double 16;

我考虑过是否是编译器优化的结果,但我看了下发现我的编译器优化是被关掉的,而且struct和clas的对齐方式是默认default,所以得出以上结论,仅供参考!

 


受你问题的启发,这几研究了下计算机组成原理,其中看了一本书叫Write Great Code Vol1——Understanding The Machine,在内存结构那章有这么一段:

Figure 7-8 also suggests that compilers pack the fields into adjacent memory locations with no gaps between the fields. While this is true for many languages, this certainly isn't the most common memory organization for a record. For performance reasons, most compilers will actually align the fields of a record on appropriate memory boundaries. The exact details vary by language, compiler implementation, and CPU, but a typical compiler will place fields at an offset within the record's storage area that is 'natural' for that particular field's data type. On the 80x86, for example, compilers that follow the Intel ABI (application binary interface) will allocate one-byte objects at any offset within the record, words only at even offsets, and double-word or larger objects on double-word boundaries. Although not all 80x86 compilers support the Intel ABI, most do, which allows records to be shared among functions and procedures written in different languages on the 80x86. Other CPU manufacturers provide their own ABI for their processors and programs that adhere to an ABI can share binary data at run time with other programs that adhere to the same ABI.

说的是关于结构体中数据的对齐方式因语言、编译器和CPU而异,但是通常的做法就是把数据成员放置在与该成员的类型较为符合(也称自然放置)的地方。

因为Intel的 Pentium 系列处理器属于x86家族,x86家族中:

8088, 8086, 80186, 80188 只有20条数据总线,寻址能力为1M

80286, 80386sx 有24条数据总线 可寻址16M

80386dx 有32条数据总线 可寻址4GB

80486, Pentium 也是32条 寻址4GB

Pentium Pro, II, III, IV 有36条数据总线 可寻址64GB

P4应该属于Prntium Pro,有36条数据总线,我的也是P42.4G,但x86家族的通用整型寄存器是32位的,也就是在每个CPU时钟周期中最多这能处理32位数据。

一般所说的32,64位CPU都指的是其数据总线数量,而不是CPU寄存器所以P4是属于32位的处理器(以后买CPU要看寄存器大小不要被表面的32, 64云云,给蒙啦...)

所以Pentium Pro多出来的那4条总线P用没有!

因为有32条能用到的数据总线,所以每个cpu指令可操作的对象最大可能就为32bit,即8个位,x86家族采用2个memory bank表示一个字,所以32位数据总线可由4个bank来表示,所以其内部内存模型为: 
QW: quad-word

.^ 
.| 
.| 
QWX2: XX XX XX XX // 从左往右依次是bank 1, 2, 3, 4 
QWX1: XX XX XX XX // 8位 X表数据未知 
QWX0: XX XX XX XX // 8位 bank中的每个X表示一个位 
.| 
.|地址由下往上增 

// 右边的数字每一个表示一个位

而x86CPU对打包数据体(结构,联合,这里联合被排除)中的数据类型的排列方式是遵循以下原则的:

1、Compiler will allocate one-byte objects at any offset within the record.

编译器将在结构基地址的任意偏移量处分配1字节(这就是为什么创建一个空类也占1字节的原因)。

2、Words only at even offsets。

字只处在偏移量为偶数的地方。

3、Double-word or larger objects on double-word boundaries.

双字或更大的数据体只处在双字的边缘上。

以上是我看到的一点心得,应该是Intel处理器和大部分编译器的默认处理方式,给你分享下。

另外我又试验了以后发现竟然是VC的问题,我在除VC外的其他几个编译器上试验的结果都是16位,对齐原则和我跟你说的一样,VC为什么是24呢?因为它编译器里的默认对齐方式是8字节,请看以下结果:

#include <iostream> 
using namespace std;

//#pragma pack()

class dd{ 
public: 
int a; 
double c; 
virtual ~dd(){} 
}a, b;

int main() 

cout << &a << endl; 
cout << &a.a << endl; 
cout << &a.c << endl; 
cout << &b; 
}


运行结果是

0041E008 // vptr 
0041E010 // int 
0041E018 // double 
0041E020

内存模型可能就是这样: 
X: 填充字节

.^ 
.| 
.| 
QWX2: HH HH HH HH // double 
QWX1: XX XX BB BB // int 
QWX0: XX XX AA AA // vptr 
.| 
.|地址由下往上增 
.| 
所以是3*8 = 24字节

可见编译器把vptr也分配为了8字节,可能有4字节的填充,原因我想很简单,就是VC迎合32位寄存器的CPU的结果,因为这样就可以每次读取的最大32位数据都可以在一个32位的通用整型寄存器中来执行操作,只需要一个指令就可以把数据分离出来,而如果是以4字节对齐的话,就需要2个指令,先把寄存器中的高16位(由于intel是反着存的)读入16位通用整型寄存器(AX,BX,CX,DX),再把数据提取出来。 
这样的结果是增加了程序执行的内存开销,但提高了执行速度,典型的牺牲空间换取时间的做法(貌似现在都以时间为优先考虑,计算机的RAM是越来越大,就连cache都有好几级大到数M了-_-~)。


但在其他的编译器上产生的结果就是16,总之#pragma 宏的实现本来就是最不可靠的,因为很大程度上它的实现是取决与编译器和CPU,所以遇到这样的问题还是要结合多方面考虑,另外多吸收一些计算机结构的底层知识也能帮助自己的理解(说“帮助”有点牵强,因为了解了底层实现那还有什么高级“花招”能忽悠得了我的^_^)。

PS: 多谢你的这个问题啊呵呵,我这段时间读了3本计算机结构的书,对底层有了一定了解,今后打算把Intel的3本Developer manual也搞定,哈哈过瘾!!

 

 

华为的比试题,内存对齐,请教高手详解
悬赏分:5 - 解决时间:2008-11-7 16:40
还是看个例子把,华为的比试题 
struct tagAAA 

unsigned char ucld:1; 
unsigned char ucpara0:2; 
unsigned char ucstate:6; 
unsigned char uctail:4; 
unsigned char ucavail; 
unsigned char uctail2:4; 
unsigned long uldate; 
}AAA_s; 
问:AAA_s在字节对齐分别为1,4的情况下占用空间大小是多少?还有那个冒号是什么意思啊
提问者: xingdabo5921 - 初入江湖 三级 最佳答案
位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

struct 位域结构名

{ 位域列表 };

其中位域列表的形式为: 类型说明符 位域名:位域长度

例如:

struct bs 

int a:8; 
int b:2; 
int c:6; 
};

位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:

struct bs 

int a:8; 
int b:2; 
int c:6; 
}data;

说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:

1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

struct bs 

unsigned a:4 
unsigned :0 /*空域*/ 
unsigned b:4 /*从下一单元开始存放*/ 
unsigned c:4 
}

在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k 

int a:1 
int :2 /*该2位不能使用*/ 
int b:3 
int c:2 
};

从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

二、位域的使用

位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名 位域允许用各种格式输出。

main(){ 
struct bs 

unsigned a:1; 
unsigned b:3; 
unsigned c:4; 
} bit,*pbit; 
bit.a=1; 
bit.b=7; 
bit.c=15; 
printf("%d,%d,%d/n",bit.a,bit.b,bit.c); 
pbit=&bit; 
pbit->a=0; 
pbit->b&=3; 
pbit->c|=1; 
printf("%d,%d,%d/n",pbit->a,pbit->b,pbit->c); 
}

上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。

程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=", 该行相当于: pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=", 相当于: pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值