为什么 sizeof 运算符返回的结构大小大于结构成员的总大小?
sizeof
这是因为添加了填充以满足对齐约束 . Data structure alignment会影响程序的性能和正确性:
错误对齐的访问可能是一个硬错误(通常是 SIGBUS ) .
SIGBUS
错误对齐的访问可能是软错误 .
在硬件中进行了更正,以适度降低性能 .
或通过软件仿真进行纠正,以避免严重的性能下降 .
此外,原子性和其他并发保证可能会被破坏,从而导致细微的错误 .
以下是使用x86处理器的典型设置(所有使用的32位和64位模式)的示例:
struct X { short s; /* 2 bytes */ /* 2 padding bytes */ int i; /* 4 bytes */ char c; /* 1 byte */ /* 3 padding bytes */ }; struct Y { int i; /* 4 bytes */ char c; /* 1 byte */ /* 1 padding byte */ short s; /* 2 bytes */ }; struct Z { int i; /* 4 bytes */ short s; /* 2 bytes */ char c; /* 1 byte */ /* 1 padding byte */ }; const int sizeX = sizeof(struct X); /* = 12 */ const int sizeY = sizeof(struct Y); /* = 8 */ const int sizeZ = sizeof(struct Z); /* = 8 */
可以通过对齐排序成员来最小化结构的大小(按基本类型的大小排序)(如上例中的结构 Z ) .
Z
重要说明:C和C标准都声明结构对齐是实现定义的 . 因此,每个编译器可能选择以不同方式对齐数据,从而导致不同且不兼容的数据布局 . 因此,在处理将由不同编译器使用的库时,了解编译器如何对齐数据非常重要 . 某些编译器具有命令行设置和/或特殊 #pragma 语句来更改结构对齐设置 .
#pragma
打包和字节对齐,如C FAQ _663974中所述:
这是为了对齐 . 许多处理器如果以各种方式填充,则无法访问2字节和4字节数量(例如,整数和长整数) . 假设你有这个结构:struct {char a [3];short int b;long int c;char d [3];};现在,您可能认为应该可以将此结构打包到内存中:------- ------- ------- -------|一个| b |
| b | c |
| c | d |
但是如果编译器像这样安排它,它在处理器上要容易得多:------- ------- -------|一个|
| b |
| c |
| d |
在打包版本中,请注意你和我看到b和c字段如何环绕至少有点困难?简而言之,处理器也很难 . 因此,大多数编译器将填充结构(好像有额外的,不可见的字段),如下所示:------- ------- ------- -------|一个| pad1 |
| b | pad2 |
| d | pad3 |
如果您希望结构具有GCC的特定尺寸,例如使用attribute((packed)) .
在Windows上,将cl.exe编译器与/Zp option一起使用时,可以将对齐设置为一个字节 .
通常,CPU更容易访问4(或8)的倍数,具体取决于平台和编译器 .
所以这基本上是一个对齐的问题 .
You need to have good reasons to change it.
这可能是由于字节对齐和填充,因此结构在平台上出现偶数个字节(或单词) . 例如在Linux上的C语言中,有以下3种结构:
#include "stdio.h" struct oneInt { int x; }; struct twoInts { int x; int y; }; struct someBits { int x:2; int y:6; }; int main (int argc, char** argv) { printf("oneInt=%zu\n",sizeof(struct oneInt)); printf("twoInts=%zu\n",sizeof(struct twoInts)); printf("someBits=%zu\n",sizeof(struct someBits)); return 0; }
具有大小(以字节为单位)的成员分别是4字节(32位),8字节(2x 32位)和1字节(2 6位) . 上面的程序(在Linux上使用gcc)将大小打印为4,8和4 - 其中最后一个结构被填充,因此它是一个单词(在我的32位平台上为4 x 8位字节) .
oneInt=4 twoInts=8 someBits=4
也可以看看:
对于Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
和GCC声称与微软的编译器兼容:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
除了以前的答案,请注意无论包装, there is no members-order-guarantee in C++ . 编译器可以(当然也可以)将虚拟表指针和基础结构的成员添加到结构中 . 标准不能确保虚拟表的存在(未指定虚拟机制实现),因此可以得出结论,这种保证是不可能的 .
我非常确定 member-order is guaranteed in C ,但在编写跨平台或交叉编译程序时,我不会指望它 .
由于所谓的包装,结构的尺寸大于其部件的总和 . 特定处理器具有与其一起使用的优选数据大小 . 大多数现代处理器的首选大小,如果是32位(4字节) . 当数据在这种边界上时访问存储器比跨越该大小边界的事物更有效 .
例如 . 考虑一下简单的结构:
struct myStruct { int a; char b; int c; } data;
如果机器是32位机器,数据在32位边界上对齐,我们看到一个直接的问题(假设没有结构对齐) . 在这个例子中,让我们假设结构数据从地址1024开始(0x400 - 注意最低的2位为零,因此数据与32位边界对齐) . 对data.a的访问将正常工作,因为它从边界开始 - 0x400 . 对data.b的访问也可以正常工作,因为它位于地址0x404 - 另一个32位边界 . 但是未对齐的结构会将data.c放在地址0x405处 . data.c的4个字节位于0x405,0x406,0x407,0x408 . 在32位机器上,系统将在一个存储器周期内读取data.c,但只能获得4个字节中的3个(第4个字节位于下一个边界) . 因此,系统必须进行第二次内存访问才能获得第4个字节,
现在,如果不是将data.c放在地址0x405,编译器将结构填充3个字节并将data.c放在地址0x408,那么系统只需要1个周期来读取数据,从而缩短了对该数据元素的访问时间减少50% . 填充交换内存效率以提高处理效率 . 鉴于计算机可以拥有大量内存(许多千兆字节),编译器认为交换(速度超过大小)是合理的 .
不幸的是,当您尝试通过网络发送结构甚至将二进制数据写入二进制文件时,此问题将成为杀手 . 在结构或类的元素之间插入的填充可以破坏发送到文件或网络的数据 . 为了编写可移植代码(一个将转到几个不同的编译器),您可能必须分别访问结构的每个元素以确保正确的“打包” .
另一方面,不同的编译器具有不同的管理数据结构打包的能力 . 例如,在Visual C / C中,编译器支持#pragma pack命令 . 这将允许您调整数据打包和对齐 .
例如:
#pragma pack 1 struct MyStruct { int a; char b; int c; short d; } myData; I = sizeof(myData);
我现在应该有11的长度 . 没有编译指示,我可以是11到14之间的任何东西(对于某些系统,多达32个),具体取决于编译器的默认打包 .
如果您隐式或显式设置结构的对齐方式,它可以这样做 . 对齐4的结构将始终是4个字节的倍数,即使其成员的大小不是4个字节的倍数 .
也可以在x86下使用32位整数编译库,如果您手动执行此操作,您可能会在64位进程上比较其组件会产生不同的结果 .
C99 N1256 standard draft
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 sizeof运算符:
3当应用于具有结构或联合类型的操作数时,结果是此类对象中的总字节数,包括内部和尾部填充 .
6.7.2.1结构和联合说明符:
13 ...结构对象中可能有未命名的填充,但不是在它的开头 .
和:
15结构或联合的末尾可能有未命名的填充 .
新的C99 flexible array member feature( struct S {int is[];}; )也可能会影响填充:
struct S {int is[];};
16作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为灵活的阵列成员 . 在大多数情况下,忽略灵活的数组成员 . 特别地,结构的尺寸好像省略了柔性阵列构件,除了它可以具有比遗漏所暗示的更多的拖尾填充 .
附件J可携带性问题重申:
未指定以下内容:...在结构或联合中存储值时填充字节的值(6.2.6.1)
C++11 N3337 standard draft
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3尺寸:
2应用于类时,结果是该类对象中的字节数,包括在数组中放置该类型对象所需的任何填充 .
9.2 class 成员:
指向标准布局结构对象的指针,使用reinterpret_cast进行适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单位),反之亦然 . [注意:因此,在标准布局结构对象中可能存在未命名的填充,但不是在其开头,以实现适当的对齐 . - 结束说明]
我只知道足够的C来理解这个注释:-)
除了其他答案之外,结构可以(但通常不)具有虚函数,在这种情况下,结构的大小也将包括vtbl的空间 .
C language leaves compiler some freedom about the location of the structural elements in the memory:
内存孔可能出现在任何两个组件之间,也可能出现在最后一个组件之后 . 这是因为目标计算机上的某些类型的对象可能受到寻址边界的限制
"memory holes" size包含在sizeof运算符的结果中 . sizeof仅包括灵活阵列的大小,可在C / C中使用
该语言的某些实现允许您通过编译指示和编译器控制结构的内存布局选项
The C language provides some assurance to the programmer of the elements layout in the structure:
编译器需要分配一系列增加内存地址的组件
第一个组件的地址与结构的起始地址一致
未命名的位字段可以包括在相邻元素的所需地址对齐的结构中
Problems related to the elements alignment:
不同的计算机以不同的方式排列对象的边缘
对位域宽度的不同限制
计算机在如何存储单词中的字节方面有所不同(英特尔80x86和摩托罗拉68000)
How alignment works:
p.s更多详细信息请点击此处:“Samuel P.Harbison,Guy L.Steele C A Reference,(5.6.2 - 5.6.7)”
我们的想法是,对于速度和缓存考虑因素,操作数应该从与其自然大小对齐的地址中读取 . 为了实现这一点,编译器将填充结构成员,以便对齐以下成员或后续结构 .
struct pixel { unsigned char red; // 0 unsigned char green; // 1 unsigned int alpha; // 4 (gotta skip to an aligned offset) unsigned char blue; // 8 (then skip 9 10 11) }; // next offset: 12
x86架构始终能够获取未对齐的地址 . 然而,它更慢并且当未对准与两个不同的高速缓存行重叠时,当对齐的访问仅驱逐一个时,它将驱逐两个高速缓存行 .
有些架构实际上必须捕获未对齐的读写,以及早期版本的ARM架构(演变成当今所有移动CPU的架构)......好吧,它们实际上只是返回了不良数据 . (他们忽略了低位 . )
最后,请注意缓存行可以任意大,并且编译器不会尝试猜测那些缓存行或进行空间与速度的权衡 . 相反,对齐决策是ABI的一部分,表示最终将均匀填充缓存行的最小对齐 .
TL; DR:对齐非常重要 .
11 回答
这是因为添加了填充以满足对齐约束 . Data structure alignment会影响程序的性能和正确性:
错误对齐的访问可能是一个硬错误(通常是
SIGBUS
) .错误对齐的访问可能是软错误 .
在硬件中进行了更正,以适度降低性能 .
或通过软件仿真进行纠正,以避免严重的性能下降 .
此外,原子性和其他并发保证可能会被破坏,从而导致细微的错误 .
以下是使用x86处理器的典型设置(所有使用的32位和64位模式)的示例:
可以通过对齐排序成员来最小化结构的大小(按基本类型的大小排序)(如上例中的结构
Z
) .重要说明:C和C标准都声明结构对齐是实现定义的 . 因此,每个编译器可能选择以不同方式对齐数据,从而导致不同且不兼容的数据布局 . 因此,在处理将由不同编译器使用的库时,了解编译器如何对齐数据非常重要 . 某些编译器具有命令行设置和/或特殊
#pragma
语句来更改结构对齐设置 .打包和字节对齐,如C FAQ _663974中所述:
| b | c |
| c | d |
但是如果编译器像这样安排它,它在处理器上要容易得多:------- ------- -------
|一个|
| b |
| c |
| d |
在打包版本中,请注意你和我看到b和c字段如何环绕至少有点困难?简而言之,处理器也很难 . 因此,大多数编译器将填充结构(好像有额外的,不可见的字段),如下所示:------- ------- ------- -------
|一个| pad1 |
| b | pad2 |
| c |
| d | pad3 |
如果您希望结构具有GCC的特定尺寸,例如使用attribute((packed)) .
在Windows上,将cl.exe编译器与/Zp option一起使用时,可以将对齐设置为一个字节 .
通常,CPU更容易访问4(或8)的倍数,具体取决于平台和编译器 .
所以这基本上是一个对齐的问题 .
You need to have good reasons to change it.
这可能是由于字节对齐和填充,因此结构在平台上出现偶数个字节(或单词) . 例如在Linux上的C语言中,有以下3种结构:
具有大小(以字节为单位)的成员分别是4字节(32位),8字节(2x 32位)和1字节(2 6位) . 上面的程序(在Linux上使用gcc)将大小打印为4,8和4 - 其中最后一个结构被填充,因此它是一个单词(在我的32位平台上为4 x 8位字节) .
也可以看看:
对于Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
和GCC声称与微软的编译器兼容:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
除了以前的答案,请注意无论包装, there is no members-order-guarantee in C++ . 编译器可以(当然也可以)将虚拟表指针和基础结构的成员添加到结构中 . 标准不能确保虚拟表的存在(未指定虚拟机制实现),因此可以得出结论,这种保证是不可能的 .
我非常确定 member-order is guaranteed in C ,但在编写跨平台或交叉编译程序时,我不会指望它 .
由于所谓的包装,结构的尺寸大于其部件的总和 . 特定处理器具有与其一起使用的优选数据大小 . 大多数现代处理器的首选大小,如果是32位(4字节) . 当数据在这种边界上时访问存储器比跨越该大小边界的事物更有效 .
例如 . 考虑一下简单的结构:
如果机器是32位机器,数据在32位边界上对齐,我们看到一个直接的问题(假设没有结构对齐) . 在这个例子中,让我们假设结构数据从地址1024开始(0x400 - 注意最低的2位为零,因此数据与32位边界对齐) . 对data.a的访问将正常工作,因为它从边界开始 - 0x400 . 对data.b的访问也可以正常工作,因为它位于地址0x404 - 另一个32位边界 . 但是未对齐的结构会将data.c放在地址0x405处 . data.c的4个字节位于0x405,0x406,0x407,0x408 . 在32位机器上,系统将在一个存储器周期内读取data.c,但只能获得4个字节中的3个(第4个字节位于下一个边界) . 因此,系统必须进行第二次内存访问才能获得第4个字节,
现在,如果不是将data.c放在地址0x405,编译器将结构填充3个字节并将data.c放在地址0x408,那么系统只需要1个周期来读取数据,从而缩短了对该数据元素的访问时间减少50% . 填充交换内存效率以提高处理效率 . 鉴于计算机可以拥有大量内存(许多千兆字节),编译器认为交换(速度超过大小)是合理的 .
不幸的是,当您尝试通过网络发送结构甚至将二进制数据写入二进制文件时,此问题将成为杀手 . 在结构或类的元素之间插入的填充可以破坏发送到文件或网络的数据 . 为了编写可移植代码(一个将转到几个不同的编译器),您可能必须分别访问结构的每个元素以确保正确的“打包” .
另一方面,不同的编译器具有不同的管理数据结构打包的能力 . 例如,在Visual C / C中,编译器支持#pragma pack命令 . 这将允许您调整数据打包和对齐 .
例如:
我现在应该有11的长度 . 没有编译指示,我可以是11到14之间的任何东西(对于某些系统,多达32个),具体取决于编译器的默认打包 .
如果您隐式或显式设置结构的对齐方式,它可以这样做 . 对齐4的结构将始终是4个字节的倍数,即使其成员的大小不是4个字节的倍数 .
也可以在x86下使用32位整数编译库,如果您手动执行此操作,您可能会在64位进程上比较其组件会产生不同的结果 .
C99 N1256 standard draft
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 sizeof运算符:
6.7.2.1结构和联合说明符:
和:
新的C99 flexible array member feature(
struct S {int is[];};
)也可能会影响填充:附件J可携带性问题重申:
C++11 N3337 standard draft
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3尺寸:
9.2 class 成员:
我只知道足够的C来理解这个注释:-)
除了其他答案之外,结构可以(但通常不)具有虚函数,在这种情况下,结构的大小也将包括vtbl的空间 .
C language leaves compiler some freedom about the location of the structural elements in the memory:
内存孔可能出现在任何两个组件之间,也可能出现在最后一个组件之后 . 这是因为目标计算机上的某些类型的对象可能受到寻址边界的限制
"memory holes" size包含在sizeof运算符的结果中 . sizeof仅包括灵活阵列的大小,可在C / C中使用
该语言的某些实现允许您通过编译指示和编译器控制结构的内存布局选项
The C language provides some assurance to the programmer of the elements layout in the structure:
编译器需要分配一系列增加内存地址的组件
第一个组件的地址与结构的起始地址一致
未命名的位字段可以包括在相邻元素的所需地址对齐的结构中
Problems related to the elements alignment:
不同的计算机以不同的方式排列对象的边缘
对位域宽度的不同限制
计算机在如何存储单词中的字节方面有所不同(英特尔80x86和摩托罗拉68000)
How alignment works:
p.s更多详细信息请点击此处:“Samuel P.Harbison,Guy L.Steele C A Reference,(5.6.2 - 5.6.7)”
我们的想法是,对于速度和缓存考虑因素,操作数应该从与其自然大小对齐的地址中读取 . 为了实现这一点,编译器将填充结构成员,以便对齐以下成员或后续结构 .
x86架构始终能够获取未对齐的地址 . 然而,它更慢并且当未对准与两个不同的高速缓存行重叠时,当对齐的访问仅驱逐一个时,它将驱逐两个高速缓存行 .
有些架构实际上必须捕获未对齐的读写,以及早期版本的ARM架构(演变成当今所有移动CPU的架构)......好吧,它们实际上只是返回了不良数据 . (他们忽略了低位 . )
最后,请注意缓存行可以任意大,并且编译器不会尝试猜测那些缓存行或进行空间与速度的权衡 . 相反,对齐决策是ABI的一部分,表示最终将均匀填充缓存行的最小对齐 .
TL; DR:对齐非常重要 .