内存地址对齐问题研究所分析

 

1.指针对齐问题

         CPU一般要求指针的值(内存地址)要与它的指向类型数据的尺寸相匹配。例如,2个字节的数据类型被访问的地址值为 2 的倍数,4个字节的数据类型(如 int)被访问的地址值是 4 的倍数,等等。一个字节的数据类型(如 char 型)对其访问地址无限制(因为是 1 的倍数)。
      
Intel处理器上,指针对齐这个问题不是致命的,至多占用CPU更多的时间进行指针转换,从而带来性能的下降;但是对于其它类型的处理器来说就是致命的了:如果访问的指针不对齐,会带来运行错误。

      
当指针从一种类型的指针转换到另外一种类型的指针的时候,就存在着产生非对齐指针的可能性。不过,正如原文中所说的,一个对齐要求较高的指针类型S转换成一个对齐要求较低的指针类型D是安全的,这种转换不会产生非对齐指针;但是,如果是一个对齐要求较低的指针类型D转换成一个对齐要求较高的指针类型S,就有可能产生非对齐指针,这种转换就是不安全。

      
c语言参考手册》中6.1.3节中提到,把一个对齐要求较高的指针类型S转换成一个对齐要求较低的指针类型D是安全的,安全指的是类型D在用于读取与存储D类型对象时可以得到预期效果,后面转换回原指针类型时能够恢复原指针。

举例说明
1)
较高的指针类型到较低的指针类型转换
      
unix上,每个int型数据占4个字节,在hp系统上,例如,十六进制表示的整数 0x1a2b3c4d 在内存中是这样存放的:
(高存储地址)

 Base Address +0 1a 
 Base Address +1 2b
 Base Address +2 3c
 Base Address +3 4d
 
(低存储地址)

 
如果有这样的程序:

 
代码
:
   int a = 0x1a2b3c4d;
   int *p = &a;
   char *q = (char *)p;
   printf("%p/n", *q);  //
执行结果:
000000000000001a  
   p=NULL;
   p = (int*)q;
   printf("%p/n", *p);  //
执行结果:
000000001a2b3c4d

      
则其中的指针 p q 的值就是 Base Addressq char型指针(重要),所以 *q 的结果得到 0x1a不就是我们期待的吗? 在这种情况下,不会得到 0x1a 以外的值,所以是也可以说是安全的。重要:搞清楚指针操作受指针基类型而不是指针所指向的对象类型支配就没问题了。

      
无论对于自己的程序还是系统来说。程序后面两句说明,从 q 指针能够恢复原来的 p 指针,从结果来看也能得到我们预期的值。

1)
较低的指针类型到较高的指针类型转换

把一个对齐要求较低的指针类型D转换成一个对齐要求较高的指针类型S是不安全的,得不到预期值。例如,char* int*的转换:
代码
:
  char c = 0x1a;
  char* p = &c;
  int* q = (int*)p;
  printf("%p/n", *q);  //
执行结果:
000000001a000000
  p=NULL;
  p = (char*)q;
  printf("%p/n", *p);  //
执行结果:000000000000001a

       (注意:上面的指针赋值在X86上没问题,但是移植到ARM芯片上会产生Bus Error 问题,所以需要进行移位转换成自己对齐!)
      
你无法预测(预期)打印出的 *q 的值是什么,因为我们除了知道整数的一个低位字节(0x1a)之外,对于这个字节后面的其余三个字节位一无所知,其值是不确定的。因此,这样的指针转换就不是安全的(更严重的情况是用 *q 写数据,会破坏掉 0x1a 后面的三个字节的数据,给程序带来错误隐患),其结果也是不能预测的。通过 q 恢复原来的指针 p 没有问题。
另外说明:

 
对于char型数组可以自然对齐。
  
例如: char[9],地址是8的倍数   可以把它的值赋给LONG型数据。
          char[5]
,地址是4的倍数
          char[3]
,地址是2的倍数

常用指针对齐代码格式:

#define ALIGNBYTES  N   ///N = 1,2,4,8 需要对齐的字节数

#define ALIGN(p)  (((TYPE)(p) + ALIGNBYTES) & ~ALIGNBYTES)  ///TYPE:要对齐的数据类型

例如:

#define    ALIGNBYTES    3

#define    ALIGN(p)    (((u_int)(p) + ALIGNBYTES) &~ ALIGNBYTES)

(u_int)(p) 把指针p转换到u_int(应该是unsigned int)类型,这个没问题吧。
简化一下,就是
  (p+3) & (~3)
3的二进制是0000....0011,也就是只有最低两位为1,前面其他位都是
0
~3 是对3取反,1111....1100,也就是只有最低两位为0,前面其他位都是
1
x&(~3) 任何一个数和~3做与运算,结果是把最低两位清0,前面其他位都保持不变。这就达到了对齐的效果
p+3的意思是向上对齐。如果不加3,直接p&(~3),就是向下对齐。

对齐的结果最低两位永远是0,结果都是4的倍数。
假定p=4,0100,p+3就是7,0111,(p+3)&(~3)就是
4
假定p=5,0101,p+3就是8,1000,(p+3)&(~3)就是
8
假定p=6,0110,p+3就是9,1001,(p+3)&(~3)就是
8
假定p=7,0111,p+3就是10,1010,(p+3)&(~3)就是8

我的理解是
对奇或者不对齐
指针指向的地址会改变,但是其地址保存的值是不变的。
对齐不过是多分配些不用的空间,提高寻址的速度。

保证低两位是0...地址值是4的整数倍...32位机.地址一般都是4字节...这样比较有效率....

 

2. 字节对齐问题

         一、什么是对齐,以及为什么要对齐:

1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

二、对齐的实现

         通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A {
    int a;
    char b;
    short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcut A)值为8。
现在把该结构体调整成员变量的顺序。
struct B {
    char b;
    int a;
    short c;
};
这时候同样是总共7个字节的变量,但是sizeof(struct B)的值却是12。
下面我们使用预编译指令#pragma pack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐
*/
struct C {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐
*/
sizeof(struct C)值是8。

修改对齐值为1:
#progma pack (1) /*指定按1字节对齐
*/
struct D {
    char b;
    int a;
    short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐
*/
sizeof(struct D)值为7。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1)数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。

2)指定对齐值:#pragma pack (value)时的指定对齐值value。

3)
结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。


       有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不难理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B {
    char b;
    int a;
    short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为 4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节
,sizeof(struct B)=12;

同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐
*/
struct C {
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐
*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以 C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以
sizeof(struct C)=8.

有了以上的解释,相信你对C语言的字节对齐概念应该有了清楚的认识了吧。在网络程序中,掌握这个概念可是很重要的喔,在不同平台之间(比如在Windows 和Linux之间)传递2进制流(比如结构体),那么在这两个平台间必须要定义相同的对齐方式,不然莫名其妙的出了一些错,可是很难排查。

 

字节对齐(强制对齐以及自然对齐)

struct {}node;

32为的x86,window下VC下sizeof(node)的值为1,而linux的gcc下值为0;


一、WINDOWS下(VC--其实GCC和其原理基本一样,象这种问题,一般要查具体的编译器设置)字节对齐的规则:

1、一般设置的对齐方式为1,2,4字节对齐方式,VC一般默认为4字节(最大为8字节)。结构的首地址必须是结构内最宽类型的整数倍地址;另外,结构体的每一个成员起始地址必须是自身类型大小的整数倍(需要特别注意的是windows下是这样的,但在linux的gcc编译器下最高为4字节对齐),否则在前一类型后补0;这里特别提到的是数组一定要注意,而且在一些编程的技巧中,我们可以使用数组强制字节达到对齐的目的。这在网络编程中是很常见的。

举例:比如CHAR型占用空间为1字节,则其起始位置必须可被1整除。INT为4字节,其起始位置必须被4整除,依次类推。(我们假定类或结构体的起始位置为0位置,其实编译器是在开辟空间时,会寻找起始位置可被结构内最宽类型整除的地址做为开始地址,因此我们可以假定其为0值,因为这0值可以被任意的类型整除。)

2、结构体的整体大小必须可被对齐值整除,默认4(默认,且结构中的类型大小都小于默认的4)。

3、结构体的整体大小必须可被本结构内的最宽类型整除。(其实和上一条是一样的,但这里独立出来,起注意作用。比如结构体里的有DOUBLE,那么结构的大小最后必须可被8整除)

注意:GCC不是这样,就是最高只能被4整除,它是个死的。

否则(2、3条),编译器会在结构的最后添充一定的特定字符来补齐。

struct T
{
char ch;
double d ;
};

在VC中是16个字节,GCC中为12个字节。

4、对于结构体内嵌套结构体的形势,规定是必须按照基本数据类型来定义,而不能以嵌套结构大小来做为上三种使用的基准。

二、举例:

struct A
{
int a;
char b;
short c;
};
struct B
{
char b;
int a;
short c;
};
struct C
{
double t;
char b;
int a;
short c;
};
struct D
{
char b;
double t;
int a;
short c;
};

在VC中,SIZEOF这四个结构体,分别为:8、12、24、24;

我们先谈第一个,(说明一下,在考虑结构体大小时,我们基本可以忽略起始地址的问题,因为这个编译器会自动为我们做好,见上面的说明),结构体内首先是一个INT的4字节,起始地址假定为0,整除4,其小于等于默认的4字节对齐且0为4(INT的占用空间)的整数倍,所以,其占四个字节;其后为起始地址为5,空间为1个字节的CHAR,小于4且5为1(CHAR占用空间)的整数倍,故占用1个字节,然后是一个起始地址为5占2个字节的SHORT,其小于4,但5不为2位数,故补齐一个字节,从第6个字节开始,占2字节空间。所以共占用4+1+1(补)+2=8;8/4=2;整除,故占用8字节空间。

再谈第2个,CHAR不用解释,占有一个字节空间,且可以被0地址整除。而INT则占4字节空间,所以其必须在CHAR后补齐3字节,到第四个字节,才是INT的真正地址。SHORT也不用说,所以共占有:1+3(补)+4+2=10个字节,但10不能整除4,所以要在结构体最后补齐2字节。故实际占有10+2= 12个字节。

谈第三个,C结构体只是在B结构体前加了一个DOUBLE,其它都一样,按说应该是20个字节啊,但注意我们上面规则的第3条。必须是最宽类型的整数倍,一定要分清,所以得补齐到24,D结构体类似,不再讲。

三、结构体的中含有位域

这个东西用得比较少,但还是总结一下:

如果结构体中含有位域(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个字节。

四、字节对齐的控制方法

主要是使用:

#pragma pack (2) /*指定按2字节对齐*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/

大家如果有兴趣,可以自己上机调一下各种对齐方式下的占用空间大小,这里就不再举例。

#pragma pack(push) //保存对齐状态
#pragma pack(4)//设定为4字节对齐

struct test
{
char m1;
double m4;
int m3;
};
#pragma pack(pop)//恢复对齐状态

这里需要注意的是,如果对齐的字节非为1、2、4、8等可整除位数,则自动默认回默认的对齐字节数,这个我没有测试,大家可以试一下,应该没什么问题。

五、多编译器的使用:(其下为转载

为了防止不同编译器对齐不一样,建议在代码里面指定对齐参数

可能重要的一点是关于紧缩结构的。紧缩结构的用途其实最常用的结构对齐选项就是:默认对齐和紧缩。在两个程序,或者两个平台之间传递数据时,我们通常会将数据结构设置为紧缩的。这样不仅可以减小通信量,还可以避免对齐带来的麻烦。假设甲乙双方进行跨平台通信,甲方使用了“/Zp2”这么奇怪的对齐选项,而乙方的编译器不支持这种对齐方式,那么乙方就可以理解什么叫欲哭无泪了。当我们需要一个字节一个字节访问结构数据时,我们通常都会希望结构是紧缩的,这样就不必考虑哪个字节是填充字节了。我们把数据保存到非易失设备时,通常也会采用紧缩结构,既减小存储量,也方便其它程序读出。各编译器都支持结构的紧缩,即连续排列结构的各成员变量,各成员变量之间没有任何填充字节。这时,结构的大小等于各成员变量大小的和。紧缩结构的变量可以放在1n边界,即任意地址边界。在GNU gcc:

typedef struct St2Tag

{

St1 st1;

char ch2;

}

__attribute__ ((packed)) St2;

在ARMCC:

typedef __packed struct St2Tag

{

St1 st1;

char ch2;

} St2;

在VC:

#pragma pack(1)

typedef struct St2Tag

{

St1 st1;

char ch2;

} St2;

#pragma pack()

针对不同的编译器:

#ifdef __GNUC__

#define GNUC_PACKED __attribute__ ((packed))

#else

#define GNUC_PACKED

#endif

#ifdef __arm

#define ARM_PACKED __packed

#else

#define ARM_PACKED

#endif

#ifdef WIN32

#pragma pack(1)

#endif

typedef ARM_PACKED struct St2Tag

{

St1 st1;

char ch2;

}

GNUC_PACKED St2;

#ifdef WIN32

#pragma pack()

#endif

最后记录一个小细节。gcc编译器和VC编译器都支持在紧缩结构中包含非紧缩结构,例如前面例子中的St2可以包含非紧缩的St1。但对于ARM编译器而言,紧缩结构包含的其它结构必须是紧缩的。如果紧缩的St2包含了非紧缩的St1,编译时就会报错:

 

 

 

 

C语言的字节对齐及#pragma pack的使用

C编译器的缺省字节对齐方式(自然对界)

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。

在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储(成员之间可能有插入的空字节),第一个成员的地址和整个结构的地址相同。

C编译器缺省的结构成员自然对界条件为“N字节对齐”,N即该成员数据类型的长度。如int型成员的自然对界条件为4字节对齐,而double类型的结构成员的自然对界条件为8字节对齐。若该成员的起始偏移不位于该成员的“默认自然对界条件”上,则在前一个节面后面添加适当个数的空字节。

C编译器缺省的结构整体的自然对界条件为:该结构所有成员中要求的最大自然对界条件。若结构体各成员长度之和不为“结构整体自然对界条件的整数倍,则在最后一个成员后填充空字节。

例子1(分析结构各成员的默认字节对界条界条件和结构整体的默认字节对界条件):

struct Test
{
char x1; // 成员x1为char型(其起始地址必须1字节对界),其偏移地址为
0

char x2; // 成员x2为char型(其起始地址必须1字节对界,其偏移地址为
1

float x3; // 成员x3为float型(其起始地址必须4字节对界),编译器在x2和x3之间填充了两个空字节,其偏移地址为
4

char x4; // 成员x4为char型(其起始地址必须1字节对界),其偏移地址为
8
};

因为Test结构体中,最大的成员为flaot x3,因些此结构体的自然对界条件为4字节对齐。则结构体长度就为12字节,内存布局为1100 1111 1000。

例子2:

#include <stdio.h>
//#pragma pack(2)
typedef struct
{
  int aa1; //4个字节对齐
1111
  char bb1;//1个字节对齐
1
  short cc1;//2个字节对齐
011
  char dd1; //1个字节对齐
1
  } testlength1;
int length1 = sizeof(testlength1); //4个字节对齐,占用字节
1111 1011 1000,length = 12

typedef struct
{
  char bb2;//1个字节对齐
1
  int aa2; //4个字节对齐
01111
  short cc2;//2个字节对齐
11
  char dd2; //1个字节对齐
1
  } testlength2;
int length2 = sizeof(testlength2); //4个字节对齐,占用字节
1011  1111 1000,length = 12


typedef struct
{
  char bb3; //1个字节对齐
1
  char dd3; //1个字节对齐
1
  int aa3; //4个字节对齐
001111
  short cc23//2个字节对齐
11

  } testlength3;
int length3 = sizeof(testlength3); //4个字节对齐,占用字节
1100 1111 1100,length = 12


typedef struct
{
  char bb4; //1个字节对齐
1
  char dd4; //1个字节对齐
1
  short cc4;//2个字节对齐
11
  int aa4; //4个字节对齐
1111
  } testlength4;
int length4 = sizeof(testlength4); //4个字节对齐,占用字节
1111 1111,length = 8


int main(void)
{
  printf("length1 = %d.\n",length1);
  printf("length2 = %d.\n",length2);
  printf("length3 = %d.\n",length3);
  printf("length4 = %d.\n",length4);
  return 0;
}

改变缺省的对界条件(指定对界)
·
使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
·
使用伪指令#pragma pack (),取消自定义字节对齐方式。

这时,对齐规则为:

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

结合1、2推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

因此,当使用伪指令#pragma pack (2)时,Test结构体的大小为8,内存布局为11 11 11 10。

需要注意一点,当结构体中包含一个子结构体时,子结构中的成员按照#pragma pack指定的数值和子结构最大数据成员长度中,比较小的那个进行进行对齐。例子如下:

#pragma pack(8)
struct s1{
short a;
long b;
};

struct s2{
char c;
s1 d;
long long e;
};
#pragma pack()

sizeof(s2)的结果为24。S1的内存布局为1100 1111,S2的内存布局为1000 1100 1111 0000 1111 1111。

例子:

#include <stdio.h>
#pragma pack(2)
typedef struct
{
  int aa1; //2个字节对齐
1111
  char bb1;//1个字节对齐
1
  short cc1;//2个字节对齐
011
  char dd1; //1个字节对齐
1
  } testlength1;
int length1 = sizeof(testlength1); //2个字节对齐,占用字节
11 11 10 11 10,length = 10

typedef struct
{
  char bb2;//1个字节对齐
1
  int aa2; //2个字节对齐
01111
  short cc2;//2个字节对齐
11
  char dd2; //1个字节对齐
1
  } testlength2;
int length2 = sizeof(testlength2); //2个字节对齐,占用字节
10 11 11 11 10,length = 10


typedef struct
{
  char bb3; //1个字节对齐
1
  char dd3; //1个字节对齐
1
  int aa3; //2个字节对齐
11 11
  short cc23//2个字节对齐
11

  } testlength3;
int length3 = sizeof(testlength3); //2个字节对齐,占用字节
11 11 11 11,length = 8


typedef struct
{
  char bb4; //1个字节对齐
1
  char dd4; //1个字节对齐
1
  short cc4;//2个字节对齐
11
  int aa4; //2个字节对齐
11 11
  } testlength4;
int length4 = sizeof(testlength4); //2个字节对齐,占用字节
11 11 11 11,length = 8


int main(void)
{
  printf("length1 = %d.\n",length1);
  printf("length2 = %d.\n",length2);
  printf("length3 = %d.\n",length3);
  printf("length4 = %d.\n",length4);
  return 0;
}

另外,还有如下的一种方式:

· __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

· __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

 

编写跨平台的软件入门——有关字节对齐

 

一为什么要跨平台?

你想过把你的 Windows 上编写的程序在 Linux 编译运行吗,以及在 Mac 或其他 OS 上运行等等?反过来也一样?这就需要涉及到跨平台编程知识。这里需要注意的是,平时很多在一个平台运行的程序在跨平台的时候变的不再正确。

Java 并非真的是跨平台的开发环境,它是运行在它自己的平台上。这里主要关注 C 和 C++ 的跨平台开发。

下面主要就几个方面来讨论跨平台编程的注意事项:

1.  字节序

2.  字节填充

3.  其他

二,             字节序

大家都知道计算机使用两种字节序,一种是 little-endian ,另一种是 big-endian 。这主要是由于当前流行的 CPU 之间的差异造成的,基本上是 IBM-PowerPC 使用的大序,而其他 CPU 使用的小序。

这里先来介绍一下 little-endian 和 big-endian 之间的具体差异。

X86 指令集合使用小序( little-endian )字节顺序;这就意味着多个字节值的最重要字节在地址的最低位。小序很早就使用,因为硬件容易实现,但和今天的制造商技术有点不同;但在第一代 IBM PC 机的 Vaxen 和 8086 处理器使用是它如此流行的主要原因。

看一个例子:

short example[2] = {0x0001,0x3002};

 

按照 16 进制的形式来显示上面数据在内存中的存储方式:

01 00 02 03

我们看到对于数组的第一个元素,高 8 位应该是 0 ,而最终存储的时候是在低 8 位的后面。

而另一方面 PowerPC 和 Sparc 芯片是 big-endian 的,也就是说,最重要的字节存储在较低的地址。对于 CPU 需要额外的电路实现这个功能,但对于今天的处理器技术与缓存控制技术相比较显的微不足道。使用 BIG-ENDIAN 的最大好处是在使用低级调式器时比较容易理解数据的存储,同样对于文件十六进制 DUMP 或网络 Sniffer 显示也是一样的。

对于 BIG-ENDIAN ,上面的例子中内存如下表示:

00 01 03 02

这里需要注意的是:由于 BIG-ENDIAN 格式的 RAW 数据比较容易调式,如果我们有机会设计一个新的文件格式,那么使用 BIG-ENDIAN 格式,而不是根据 CPU 架构来决定。

下面看几个关于字节序的问题:

1.  Long 型指针和 char 指针之间的转换

看下面这段代码

unsigned long value = 0x03020100;

unsigned long *ptr = &value;

unsigned char charVal;

charVal = *(unsigned char *)ptr;

程序的含义比较简单,主要是从一个指向 long 的指针强制转换为一个指向 char 的指针,这里假设指针指向的是最不重要的字节地址。

在一个 little-endian 处理器上, charVal 是 0 ,而在一个 big-endian 处理器上, charVal 的值是 3 。这样的问题是最难以发现的问题之一。

为了避免这个错误,使用一个临时变量可以解决这个问题,如下:

unsigned long temp = *ptr;

charVal = (unsigned char)temp;

上面的第二行代码就保证将在任何架构上都将最不重要的字节传递给 charVal ;编译器处理具体的细节。

2.  读写文件和写网络数据

在从文件读数据或写数据到文件的时候以及网络,对于字节顺序的处理一定要小心;一定记住不能将多个字节的数据写到文件或网络上;例如:

long val = 1;

int result = write(fileDes,&val,sizeof(val));

这段代码在 little-endian 和 big-endian 机器上执行的结果是不一样的,如果读数据的时候使用如下代码:

long val ;

int result = read(fileDes,&val,sizeof(long));

如果这两段代码分别位于 little-endian 和 big-endian 机器上,那么最终得到的 val 不是 1 ,而是 0x01000000 。

解决多字节的读写有很多办法,这里提供两种。

方法 1 :

写的代码

long val = 1;

char buf[4];

buf[0] = 0xff&val;

buf[1] = (0xff00&val)>>8;

buf[2] = (0xff0000&val)>>16;

buf[3] = (0xff000000&val)>>24;

int result = write(fileDes,buf,4);

读的代码

long val;

char buf[4];

int result = read(fileDes,buf,4);

val = buf[0]|(buf[1]<<8)|(buf[2]<<16)|(buf[3]<<24);

3.  运行时检查字节顺序

bool gIsBigEndian;

void InitializeEndianFlag()

{

Short one = 1;

Char *cp = (char *)&one;

If(*cp == 0)

    gIsBigEndian = true;

else

    gIsBigEndian = false;

return ;

}

4.  字节交换对性能的影响

由于字节顺序的问题导致在处理的时候需要进行字节交换或类似 2 中方法 1 的处理,这里称为交换。通常情况下,做字节顺序的交换并不影响,因为交换两个字节或四个字节值只需要很少的 CPU 指令,并且完全可以在寄存器中执行。

但如果有很多数据需要交换,例如:一个 1024*768 位图的图像,在这么大的循环中执行是影响性能的。

另外对于 3 的运行时检查字节序的代码要查看具体的位置。如果仅仅调用一次或几次,不会影响性能,如果对于上面的这个循环中调用,对性能的影响是显著的,这个时候可以使用一个预编译宏来分别处理。例如:

#ifdef BIG_ENDIAN//big-endian

#else//little-endian

#endif//BIG_ENDIAN

 

三,             字节填充

另一个写可移植代码的注意点是结构体的字节对齐和填充。通常,在单个平台上,如果需要保存一个结构体到文件,那么是作为一个整体写到文件的,如下:

struct myStruct{

char theChar;

long theLong;

};

struct myStruct foo;

foo.the Char = 1;

foo.theLong = 2;

如果我们已经将数据按照 big-endian 进行了交换,然后直接将整个结构体写到文件中。那么什么样的数据会被写到磁盘上呢?

int result = write(fileDes, &foo, sizeof(foo));

实际上我们不知道具体写了什么数据,因为我们还不知道这个代码在什么平台上运行;实际上上面的 code 中会将垃圾数据写到文件里,垃圾数据多少由 foo 分配到的内存决定的。

一种可能我们认为的情况是:

 01 00 00 00 02

但我们可能得到的这样的数据:

01 f 8 00 00 00 02

甚至是:

01 e6 a7 20 00 00 00 02

这里到底发生了什么? sizeof(foo) 是编译器和处理器依赖的。

有些处理器不能从某些位置读或写多个字节;几乎所有的都不能从奇数地址来读数据。通常他们只读那些是 sizeof ( value )倍数的地址;对于四个字节只能读地址是 4 个字节的倍数,对于 2 个字节的 short 只能读两个字节倍数的地址。如果不遵从这个字节对齐的规律,处理器会抛出一个异常并且终止程序,有些系统上会锁定机器(如果发生在 kernel 中)。

有时,读没有对齐的数据需要花费额外的时间。例如: PowerPC 能够读任何偶数地址,但对于那些不能被 4 整除的地址需要耗费额外的总线周期。为了读一个 long 数值( value )在 2 整除而不是 4 整除的地址,它将读四个字节并包括需要读的值的上面两个字节,抛弃 2 个字节,然后读另外四个包含 value 低 2 个字节的字节,同样抛弃另外两个。这与读 4 个字节对齐的地址相比需要多访问一次缓存。

为了达到字节对齐的目的,编译器会插入未命名的填充字节到结构体中。至于插入几个字节是通过编译器和 OS 或库内存分配器一起决定的。

在 Windows VC 编译器中,可以使用 #pragma 来指定字节对齐的方式。

总而言之,在定义结构的时候要按照字节边界对齐来定义,一般按照 4 个字节,如果不够就需要增加填充字段。

另外对于结构体写文件或输出到网络上,最好的办法是按照成员来逐个写入或发送,这可以避免将垃圾数据存放到文件中或传输到网络上。

 

四,             其他

下面是几个笔者在实际编写代码中发生过的错误,这里与大家一道分析一下。

1.         示例 1 :

for(int i = 0;i<1000;i++)

{

   ….

}

...

for(int i = 0;i<1000;i++)

{

...

}

上面这段代码是很普通的 C++ 代码,但这段代码不一定可以在所有的编译器中都能编译通过。主要的原因在于变量 i 的声明。

C++ 标准说:在 for 循环内部声明的变量在 for 结束的时候无效,因此可以连续使用再次在 for 循环中使用该记数器变量。但很不幸的是很多编译器都提供编译选项来让你觉得变量是否在 for 循环以后仍然有效。 VC 中默认编译选项 /Ze 用来指定 for 循环变量的局部性,但并非所有的编译器都是将这个选项作为默认编译参数;所以为了能让你的代码可以在任意平台编译通过,使用 C 风格的会有保证一点;如下:

int i = 0;

for(i = 0;i<1000;i++)

{

   ….

}

...

for(i = 0;i<1000;i++)

{

...

}

 

2.         示例 2 : int 型变量的使用

Int 型变量是一个奇怪的东西,它在 16 位机器上是 2 个字节,在 32 位机上是 4 个字节;将来可能在 64 位机上是 8 个字节。所以如果你的代码中有对 int 的使用,而你想代码可以在很多平台上运行,那么一定要注意了。看一下下面的情况:

for(int i = 0;i<65540;i++)

{

   ….

}

这个代码可能在不同的平台上得到不同的结果。如果这个代码是在 16 位机器上运行,那么得到的结果与 32 位机器上可能不同。

同样在使用 int 型变量写文件和输出到网络时都要小心这个问题。最好的办法是,在这些情况下不要使用 int 型变量; int 型变量仅仅在程序内部使用。

3.         关于 Bit field 的问题

在 C 语法中有 bit field 的语法,可以根据需要来定义一个符号具体占用的 bit 数,例如:

typedef struct tagTest
{
   char a:4;
   char b:2;
   char c:2;
}TagTest,*PTagTest;

实际上 tagTest 的字节数是 1 个字节,成员 a 占用 4 位, b 和各占用两位。这样的好处是可以针对每个成员赋值而设置指定的位的值,例如:

tagTest myTest;
myTest.a = 10;
myTest.b = 2;
myTest.c = 1;

假如你在 Windows 上是使用 VC 来编译连接上面的程序,不管如何处理,你不会发生任何问题。但现在我们假设将 myTest 放入缓冲区中,然后在 MAC 机器上取出来,那么会发生什么来?看代码:

Windows:

char buf[10];

buf[0] = myTest;

buf[2]=...

int result = send(fd,buf,10,..);

 

MAC:

char buf[10];

int ret = 0;

int result = recv(fd,buf,10,..);

PTagTest pTest = (PTagTest)&buf[0];

 

if(pTest->a == 10)

   ret = 1;

else

    ret = 0;

...

那么 ret 的值是什么呢?我们期望是 1 但,结果不是 1 。如果你通过调试器来观察一下 pTest 各成员的值你发现:

pTest->a = 6; pTest->b =2 ; pTest->c =2;

细心的读者可能发现这里的问题所在,原因在于不同的编译器对 bit field 进行了不同的处理。在 Windows 平台上, c 被放在字节的最高两位,而 a 被放在字节的最低 4 位,在 MAC 上正好相反。但一定要注意,这是编译器行为,而不是数据在传输过程中发生了字节的位交换。在 Windows 发送到网络的时候, buf[0] 的内容二进制表示为:

01 10 1010

在 MAC 上 recv 之后, buf[0] 的内容仍然与上面的相同。

为了避免这个问题,请不要在写文件或网络输出的时候使用 BIT FILED 语法,如果一定要使用请注意编译器对位处理的区别。

 五小结

其实实际工作中,大家认为自己的代码都不需要在多个平台上运行,而认为跨平台编码与自己无关;其实不然,好的编码习惯是慢慢养成的,如果大家都知道这些跨平台编码的细节,在开始写代码的时候就开始避免这样的问题,一旦有一天我们的代码需要跨平台运行或一点我们要写跨平台代码时,我们就不会无从下手,而是顺其自然,因为我们已经具备了这样的习惯。

当然这里的介绍只是一个开始,跨平台编码涉及的问题还很多,由于笔者经验的限制不能一一描述。

 

 

 

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
mimalloc是一种高效的内存分配器,它由微软研究院的Mikel Luján开发,旨在提供快速、可伸缩和空间效率高的内存分配。下面是mimalloc的内存分配代码分析: 1.初始化 在程序启动时,mimalloc先进行了一些初始化操作,例如创建线程缓存和分配一个全局的堆内存池。这些线程缓存可以用来分配小块内存,而堆内存池则用来分配大块内存。 2.内存分配 当需要分配内存时,mimalloc检查线程缓存中是否有足够的空闲内存块。如果有,它从线程缓存中分配内存,否则,它从堆内存池中分配内存。 在分配内存时,mimalloc使用了一种称为“分配表”的数据结构,它包含了不同大小的内存块的信息。当需要分配内存时,mimalloc根据所需内存块的大小查找对应的分配表,然后从该表中分配内存。如果没有找到合适的分配表,mimalloc使用分配表的最大块大小来分配内存。 3.内存释放 当需要释放内存时,mimalloc将内存块的大小加入到线程缓存的统计信息中,以便在未来分配内存时使用。如果线程缓存中的内存块达到一定数量,mimalloc将这些内存块释放回堆内存池中。 4.内存对齐 mimalloc还提供了一些内存对齐的功能。例如,它可以分配和释放对齐到2的幂次的内存块,还可以在分配内存时指定所需的对齐方式。 总的来说,mimalloc是一个高效、可伸缩和空间效率高的内存分配器。它通过使用线程缓存和堆内存池来平衡小块和大块内存的分配,并使用分配表来优化内存分配。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值