一、概述
我们知道,在缺省情况下,C编译器会为每一个 变量 或者 数据单元 按其自然边界对齐(natural
alignment) 的方式分配空间。
本文先介绍 自然边界对齐 方式,接着介绍4种改变C编译器的缺省字节对齐方式的方法 -- 即指定边界对齐。
二、自然边界对齐
什么是 自然边界对齐 呢?
我们知道,在C语言中,结构是一种 复合数据类型,它的 构成元素 可以是基本数据类型(如int 、long、float等)的变量,也可以是一些 复合数据类型(如数组、结构、联合体等)的数据单元。
首先让我们来了解下 结构体 字节对齐有哪些特点?
① 对于结构体,编译器会自动进行 成员变量 的对齐, 以提高运算效率。
缺省情况下,编译器为结构体的每个成员按其自然边界对齐方式分配空间 -- 自然边界对齐方式是编译器的默认对齐方式;
② 各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同;
③ 结构体整体的默认字节对齐大小是它的所有成员中对齐参数最大的一个;
④ 结构体长度的计算必须取所用过的所有对齐参数的整数倍,不够的话就填充(pad)空字节;
也就是说,最后整个结构体的大小是所有用过的对齐参数中最大的那个值的整数倍。对齐参数一般都是2的n次方,这样在处理数组时可以保证每一项都边界对齐。
下面一段英文摘自(),注意其中的红色字体,下文还会涉及到。
The type of each member of the structure usually has a default alignment, meaning that it will, unless
otherwise requested by the programmer, be aligned on a pre-determined boundary. The following
typical alignments are valid for compilers from (),/ (),
(DMC) and () when compiling for 32-bit x86:
- A char (one byte) will be 1-byte aligned.
- A short (two bytes) will be 2-byte aligned.
- An int (four bytes) will be 4-byte aligned.
- A long (four bytes) will be 4-byte aligned.
- A float (four bytes) will be 4-byte aligned.
- A double (eight bytes) will be 8-byte aligned on Windows and 4-byte aligned on Linux (8-byte with
-malign-double compile time option).
- A long double (ten bytes with C++Builder and DMC, eight bytes with Visual C++, twelve bytes with GCC) will be 8-byte aligned with C++Builder, 2-byte aligned with DMC, 8-byte aligned with Visual C++ and 4-byte aligned with GCC.
- Any pointer (four bytes) will be 4-byte aligned. (e.g.: char*, int*)
The only notable difference in alignment for a 64-bit system when compared to a 32-bit system is:
- A long (eight bytes) will be 8-byte aligned.
- A double (eight bytes) will be 8-byte aligned.
- A long double (eight bytes with Visual C++, sixteen bytes with GCC) will be 8-byte aligned with Visual C++ and 16-byte aligned with GCC.
- Any pointer (eight bytes) will be 8-byte aligned.
好了,让我们先看一个具体的例子:
例如, 下面的 结构体 各成员空间分配情况:
- struct test
- {
- char x1;
- short x2;
- float x3;
- char x4;
- };
结构体的第一个成员x1,其偏移地址为0,占据了第一个字节。
第二个成员x2为short类型,其起始地址必须 2字节边界对齐(2-byte aligned.) , 因此, 编译器在x2和x1之间填充了一个空字节。
结构体的第三个成员x3和第四个成员x4恰好落在其边界对齐地址上,在它们前面无需填充额外的空字节。
看下图:不同的颜色代表不同的结构体成员,白色代表填充的空白字节。
三、指点边界对齐
我们可以通过不同的方法来改变编译器的缺省对齐方式。
方法1: 使用#pragma pack
作用:
指定结构体、联合以及类成员的packing alignment;
语法:
#pragma pack( [show] | [push | pop] [, identifier], n )
说明: ① pack提供数据声明级别的控制,对定义不起作用;
② 调用pack时不指定参数,n将被设成默认值; ③ 一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降;语法具体分析: ① show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
② push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈; ③ pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略; ④ identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直identifier被pop出,如果identifier没有被找到,则忽略pop操作; ⑤ n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。使用#pragma pack规定的对齐长度,实际使用的规则是:结构体,联合体或者类的数据成员,第一个成员放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中较小的值来对齐。那么,也就是说,当#pragma pack指定的值等于或者超过所有数据成员的长度的时候,这个值的大小将不产生任何效果。
而结构体的对齐,则按照结构体中size最大的数据成员和#pragma pack指定值之间较小的值来对齐。
总结一下:
1. 每个成员采用#pragma pack指定的数值和自身的自然边界对齐方式大小 中较小的值来对齐。
2. 将结构体看做一个整体,那么这个整体的对齐方式采用 #pragma pack指定的数值 和结构体中size最大的数据成员的自然边界对齐大小 中较小的值来对齐。
这一切都是为了最小化长度,并且提高程序的效率。
说了这么多,有点晕了,让我们来看几个实例吧。
- // 例1
- #pragma pack(2)
- struct TestA
- {
- public:
- int a; // 第一个成员,放在[0,3]偏移的位置。
- char b; // 第二个成员sizeof(char)=1, #pragma pack(2), 取小值也就是1
- // 所以这个成员按1字节对齐,放在偏移[4]的位置。
- short c; // 第三个成员sizeof(short)=2, #pragma pack(2),取小值也就是2
- // 所以这个成员按2字节对齐,所以放在偏移[6,7]的位置。
- char d; // 第四个成员sizeof(short)=1, #pragma pack(2), 取小值也就是1
- // 所以这个成员按1字节对齐,放在[8]的位置。
- };
- #pragma pack()
- // struct TestA中size最大的数据成员(4),#pragma pack(2),
- // 取小值也就是2,所以sizeof(TestA)应当按照2来对齐,为10。
内存布局示意图(白色框为填充字节):
- // 例2
- #pragma pack(4)
- struct TestB
- {
- public:
- int a; // 第一个成员,放在[0,3]偏移的位置。
- char b; // 第二个成员sizeof(char)=1, #pragma pack(4), 取小值也就是1
- // 所以这个成员按1字节对齐,放在偏移[4]的位置。
- short c; // 第三个成员sizeof(short)=2, #pragma pack(4),取小值也就是2
- // 所以这个成员按2字节对齐,所以放在偏移[6,7]的位置。
- char d; // 第四个成员sizeof(short)=1, #pragma pack(4), 取小值也就是1
- // 所以这个成员按1字节对齐,放在[8]的位置。
- };
- #pragma pack()
- // struct TestB中size最大的数据成员(4),#pragma pack(4)
- // 取小值也就是4,所以sizeof(TestB)应当按照4来对齐,为12。
内存布局示意图(白色框为填充字节):
- // 例3(如果在GCC中编译,请加上-malign-double选项)
- #pragma pack(8)
- struct s1
- {
- short a; // 第一个成员,放在[0, 1]偏移的位置。
- long b; // 第二个成员sizeof(long)=4, #pragma pack(8), 取小值也就是4
- // 所以这个成员按4字节对齐,放在偏移[4~7]的位置。
- };
- // struct s1中size最大的数据成员(4), #pragma pack(8)
- // 取小值也就是4,所以sizeof(struct s1)应当按照4来对齐,为8
- struct s2
- {
- char c; // 第一个成员,放在[0]偏移的位置。
- struct s1 d; // 第二个成员为struct s1,其对齐方式是它的所有成员使用的对齐参数中最大的一个,即4。
- // 所以第二个成员d按4字节对齐,由于sizeof(d)=8, 放在偏移[4~11]的位置。
- long long e; // 第三个成员sizeof(long long)=8, #pragma pack(8), 取小值也就是8
- // 所以这个成员按8字节对齐,放在偏移[16~23]的位置。
- };
- #pragma pack()
s1的内存布局示意图:
s2的内存布局示意图:
例3中,我们如果将#pragma pack(8)改成#pragma pack(2), 结果会怎样呢?
- //(如果在GCC中编译,请加上-malign-double选项)
- #pragma pack(2)
- struct s1
- {
- short a; // 第一个成员,放在[0, 1]偏移的位置。
- long b; // 第二个成员sizeof(long)=4, #pragma pack(2), 取小值也就是2
- // 所以这个成员按4字节对齐,放在偏移[2~5]的位置。
- };
- // struct s1中size最大的数据成员(4), #pragma pack(2)
- // 取小值也就是2,所以sizeof(struct s1)应当按照2来对齐,sizeof(struct s1) = 6
- struct s2
- {
- char c; // 第一个成员,放在[0]偏移的位置。
- struct s1 d; // 第二个成员为struct s1,其对齐方式在这里就不是4了, 为什么呢?
- // 由上文的总结2得知,这里应该是2
- // 所以第二个成员d按2字节对齐,由于sizeof(d)=6, 放在偏移[2~7]的位置。
- long long e; // 第三个成员sizeof(long long)=8, #pragma pack(2), 取小值也就是2
- // 所以这个成员按2字节对齐,放在偏移[8~15]的位置。
- };
- // 所以,sizeof(s2) = 16
- #pragma pack()
再看一个实例:
- #include <stdio.h>
- #include <stdlib.h>
- struct test
- {
- char struc_char;
- int struc_int;
- short struc_short;
- double struc_double;
- }TEST;
- int main(int argc, const char *argv[])
- {
- printf("sizeof(TEST) = %d\n", sizeof(TEST));
- return 0;
- }
在gcc中得到的结果是20,在VC中输出的却是24,这是怎么回事呢?
原来gcc和VC关于double的对齐方式不同:在gcc中double类型的变量4字节对齐,而在VC中却是8字节对齐。(见上文中英文中红色字体部分)
如果想使double类型的变量在gcc中也是8字节对齐,编译时加上 -malign-double选项即可。
最后值得一提的是,对于数组,比如:char a[3];这种,它的对齐方式和分别写3个char是一样的.也就是说它还是按1个字节对齐. 如果写: typedef char Array3[3]; Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度. 不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。
方法2: 使用__attribute__((aligned (alignment)))
__attribute__((aligned (alignment))) 告诉编译器一个结构体或者类或者联合或者一个类型的变量(对象)分配地址空间时的地址对齐方式。也就是说,如果将__attribute__((aligned (alignment))) 作用于一个类型,那么该类型的变量在分配地址空间时, 其存放的地址一定按照alignment字节对齐(alignment必须是2的幂次方)。并且其占用的空间,即大小,也是alignment的整数倍,以保证在申请连续存储空间的时候,每一个元素的地址也是按照m字节对齐。__attribute__((aligned(alignment)))也可以作用于一个单独的变量。
看几个实例:
- struct A
- {
- char a;
- int b;
- unsigned short c;
- long d;
- unsigned long long e;
- char f;
- };
因为什么限制也没有,所以采用默认处理方式。因为其中有unsigned long long e,其在VC下8字节对齐,GCC默认4字节对齐,如果加上-malign-double选线则也8字节对齐。
所以GCC默认情况下,sizeof(struct A) = 4(a, 1-->4)+ 4 + 4(c, 2-->4) + 4 + 8 + 4(f, 1-->4) = 28。VC下sizeof(struct A) = 32
- struct B
- {
- char a;
- int b;
- unsigned short c;
- long d;
- unsigned long long e;
- char f;
- }__attribute__((aligned));
在struct B中,aligned没有参数,表示“让编译器根据目标机制采用最大最有益的方式对齐"。 当然,最有益应该是运行效率最高吧,呵呵。其结果是与采用__attribute__((aligned(8)))相同。 sizeof(struct B) = 8(1+4+2 ,即a, b, c)+ 8(d, 4-->8) + 8 + 8(f, 1-->8) = 32。
- struct C
- {
- char a;
- int b;
- unsigned short c;
- long d;
- unsigned long long e;
- char f;
- }__attribute__((aligned(1)));
在struct C中,试图使用__attribute__((aligned(1)))来使用1个字节方式的对齐,不过并未如愿,仍然采用了默认4个字节的对齐方式。这是与#pragma pack的区别 -- 选择其中值大的那个。
总结一下,或者说__attribute__((aligned (alignment))) 与 #pragma pack(m)有什么区别呢?最大的区别在 变量的自然边界对齐值 与 它们的指定值 之间选择哪个值!
#pragma pack(m)告诉编译器: 结构体或者C++类内部的成员变量相对于第一个变量的地址偏移的对齐方式。缺省情况下,编译器按照自然边界对齐,否则选择其中较小的值。
__attribute__((aligned (alignment))) 作用于一个类型,那么该类型的变量在分配地址空间时,其存放的地址一定按照alignment字节对齐(alignment必须是2的幂次方),并且其所占用的空间也是alignment的整数倍,以保证在申请连续存储空间的时候,每一个元素的地址也都是按照alignment字节对齐。__attribute__((aligned (alignment))) 也可以作用于一个单独的变量。当变量自然边界对齐值小于指定值时,选择指定的值,也就是寻则其中较大的值。
来看一个具体的例子:
- #include <stdio.h>
- #include <stdlib.h>
- #pragma pack(16)
- struct testa
- {
- unsigned int number;
- };
- #pragma pack()
- struct testa arr_testa[4] = { {65535}, {65535}, {65535}, {65535} };
- struct testb
- {
- unsigned int number;
- } __attribute__ ((aligned (16))) ;
- struct testb arr_testb[4] = { {65535}, {65535}, {65535}, {65535} };
- int main(int argc, const char *argv[])
- {
- printf("sizeof(struct testa) = %d, sizeof(arr_testa) = %d\n",
- sizeof(struct testa), sizeof(arr_testa));
- printf("sizeof(struct testb) = %d, sizeof(arr_testb) = %d\n",
- sizeof(struct testb), sizeof(arr_testb));
- printf("address of arr_testa[0] = %p\n", &arr_testa[0]);
- printf("address of arr_testa[1] = %p\n", &arr_testa[1]);
- printf("address of arr_testa[2] = %p\n", &arr_testa[2]);
- printf("address of arr_testa[3] = %p\n", &arr_testa[3]);
- puts("");
- printf("address of arr_testb[0] = %p\n", &arr_testb[0]);
- printf("address of arr_testb[1] = %p\n", &arr_testb[1]);
- printf("address of arr_testb[2] = %p\n", &arr_testb[2]);
- printf("address of arr_testb[3] = %p\n", &arr_testb[3]);
- return 0;
- }
在我的机器上(ubuntu 11.10, gcc 4.4.6)输出如下:
- sizeof(struct testa) = 4, sizeof(arr_testa) = 16
- sizeof(struct testb) = 16, sizeof(arr_testb) = 64
- address of arr_testa[0] = 0x804a040
- address of arr_testa[1] = 0x804a044
- address of arr_testa[2] = 0x804a048
- address of arr_testa[3] = 0x804a04c
- address of arr_testb[0] = 0x804a060
- address of arr_testb[1] = 0x804a070
- address of arr_testb[2] = 0x804a080
- address of arr_testb[3] = 0x804a090
可以看出#pragma pack(m)对数组元素的存放并没有什么影响,而__attribute__((aligned (alignment))) 影响了数组每个元素的地址,都是alignment字节对齐,好处就是增强了copy操作的效率。
方法三: 使用__attribute__ ((packed))取消结构在编译过程中的优化对齐
__attribute__ ((packed))作用于结构成员,表示该成员与前一个结构成员之间没有空洞。
举例:- struct foo
- {
- char a;
- int x[2] __attribute__ ((packed));
- };
这里packed属性作用于成员x,因而在结构成员a后没有空洞,而是立即紧跟着成员x。
__attribute__ ((packed))作用于整个结构,等同于为结构中的每个成员指定__attribute__ ((packed)),与#pragma pack(1)的作用等效。
举例:
- struct F{
- char a;
- int b;
- unsigned short c;
- long d;
- unsigned long long e;
- char f;
- __attribute__((packed));
sizeof(struct F) = 1 + 4 + 2 + 4 + 8 + 1 = 20。
注意:在使用了packed属性限定之后,GCC编译器将用字节存取命令(ARM中为LDRB或STRB指令)来访问该结构成员,而不是按照自然边界对齐方式来访问结构成员。方法四:GCC编译选项中使用-fpack-struct[=n]
如果没有指定n, 则去除所有结构中的空洞(注意这里会影响到所有的结构),即编译器不能在成员之间填充边界对齐的空字节。
如果指定n, 则n表示maximum alignment (that is, objects with default alignment requirements larger than this will be output potentially unaligned at the next fitting location)。但通常不应当使用该选项,因为这会使访问结构成员的效率降低,代码量增大(通常会增加1/3左右,当Flash空间很有限时就要认真考虑了),而且使生成的代码与没有使用该编译选项的系统库不兼容。
补充:
还有一个__declspec( align(#) ),它有点类似于__attribute__((aligned (alignment)))
__declspec( align(#) )和#pragma pack(n)有密切联系, 并且__declspec( align(#) )的优先级比#pragma pack(#)的要高。
规则是:
① 结构体每个成员的对齐值依然是 自身自然边界对齐值 与 #pragma pack(n)指定值 中较小的那个。
② 结构体整体的大小受__declspec( align(#) )的影响,最终取值是 结构体中size最大值 与 # 中较大的那个
举个例子吧:
- #pragma pack( 4 )
- __declspec( align(2) ) struct C
- {
- int i1; // 第一个成员,放在[0~3]
- double d1; // 第二个成员,vc默认8字节对齐,4比8小,所以最终4字节对齐,放在[4~11]
- int i2; // 第三个成员,大小是4,所以4字节对齐,放在[12~15]
- int i3; // 第四个成员,大小是4,所以4字节对齐,放在[16~19]
- };
- // 现在在定义含有C的D
- __declspec( align(16) ) struct D
- {
- int i1; // 第一个成员,放在[0~3]
- struct C m_d; // 第二个成员,受之前的__declspec( align(32) )的影响,优先级最高,
- // 偏移量是32,所以这里用32对齐, 所以m_d放在[32~63]
- int i2; // 第三个成员,大小是4,所以4字节对齐,放在[64~67]
- };
- // 当计算结构体的最后大小时,因为结构体接受 __declspec( align(16) ) 影响,
- // 比较 32 和 #, 32 比 #=16大,用32补完,所以最后的大小应该是大于68最小的32倍数,即96
- #pragma pack()
__attribute__((aligned (alignment)))有所不同:
- #include <stdio.h>
- #include <stdlib.h>
- #pragma pack(4)
- struct __attribute__ ((aligned (32))) C
- {
- int i1;
- double d1;
- int i2;
- int i3;
- };
- struct __attribute__ ((aligned (16))) D
- {
- int i1;
- struct C m_d;
- int i2;
- };
- #pragma pack()
- int main(int argc, const char *argv[])
- {
- printf("sizeof(struct C) = %d\n", sizeof (struct C));
- printf("sizeof(struct C) = %d\n", sizeof (struct D));
- return 0;
- }
- sizeof(struct C) = 32
- sizeof(struct C) = 48
参考链接:
( 细说结构字节对齐 )
(#pragma pack(n)和__attribute__((aligned(m)))的区别)
(pragma pack(非常有用的字节对齐用法说明))
(#pragma pack( n )和__declspec( align(#) ) 的偏移量计算方法)