为什么GCC不会针对结构体进行优化?

53

系统要求某些基本类型数据按照特定的内存地址对齐方式(如int类型按照4字节的倍数对齐,short类型按照2字节的倍数对齐等)。当然,这些可以优化以在填充方面浪费最少的空间。

我的问题是为什么GCC不自动完成这个操作?更明显的启发式方法(将变量按照占用空间从大到小排序)有什么缺陷吗?是否有些代码依赖于其结构体的物理顺序(这是一个好的想法)?

我只是问一下,因为GCC在很多方面都进行了超级优化,但在这方面却没有,我想这里肯定有一些比较酷的解释(而我并不知情)。


你可以在struct-reorg-branch中尝试-fipa-struct-reorg选项。是否有GCC关键字允许结构重排? - phuclv
7个回答

82

gcc不会重新排列结构体的元素,因为那样会违反C标准。C99标准的6.7.2.1节规定:

在结构体对象内,非位域成员和位域所在单元的地址按照它们声明的顺序递增。


12
但是,为什么要这样定义呢? - nes1983
2
@nes1983 程序员可能会假设结构体中数据的顺序,并使用掩码来获取每个部分。如果结构体被重新排序,则掩码可能不正确。 - Evo510
13
我感到疑惑。要使用掩码,您还需要了解填充,而这并不被语言保证。因此,您无法使用掩码。我有什么遗漏吗? - nes1983
@nes1983 我见过一些数值积分代码,它假设所有的输入都是按顺序浮点数。你将要积分的第一个和最后一个值的指针传递给它,它会在它们之间进行扫描。然而,你可以将信息保存在结构体中,因为除了积分,这是更方便的格式。 - Cort Ammon
1
虽然这样做会违反标准,但有一种有用的重新排序方法可以保护Linux内核免受rootkits / exploits的攻击:Linux KSPP(https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project)的一部分是一些结构字段的随机化/重新排序:http://openwall.com/lists/kernel-hardening/2017/05/26/8(介绍结构布局随机化插件),相关论文:https://sec.taylor.edu/doc/Improved_Kernel_Security_Through_Memory_Layout_Randomization.pdf(“通过内存布局随机化提高内核安全性”-DM Stanley-2013年)。 - osgx
@nes1983 有时候会做出一些假设,例如在处理网络时:connect() 调用将 sockaddr 作为其参数指定,这在 OOP 术语中类似于“基类”,而实际上你将发送一个“子类成员”,即不同的结构体(例如 sockaddr_in),内核依赖于标识你传递的类型的字段与 sockaddr 中的位置相同。 - Mr Redstoner

31

结构体经常被用作二进制文件格式和网络协议的打包顺序的表示。如果这样做,将会破坏它们的功能。此外,不同的编译器会以不同的方式进行优化,无法将两种代码链接在一起。这根本行不通。


3
这与网络或文件结构无关。事实上,BMP结构的头部紧密地包含着元素,这些元素落在编译器不熟悉的非自然边界上。 - Andrew Grant
1
错误,是的?你误解了问题。请重新阅读第二段,他谈到了结构体排序。这与填充完全不同。 - Serafina Brocious
8
你的第一点非常有道理,但我认为第二点不是。来自不同编译器的编译代码无论如何都不兼容。 - Johannes Schaub - litb
2
@JohannesSchaub-litb 那要看情况;如果两个编译器都遵循相同的ABI,它们就没有理由产生不兼容的代码。例如GCC和Clang,以及Windows上C语言的32位GCC和MSVC。 - rubenvb

12
tl;dr GCC不会重新排序结构体成员。
GCC在从源代码生成机器代码方面比我们大多数人都聪明;然而,如果它在重新排列我们的结构体方面比我们聪明,我会感到担忧,因为这些数据可能会被写入文件。如果在另一个系统上读取时,GCC决定重新排列结构体成员,那么以4个字符开头并且后面有4字节整数的结构体将变得无用。

4
直接将结构体读写到文件中是不可移植的,因为对齐方式(允许)会导致编译器/平台不兼容,详见 Stack Overflow回答。 - forumulator

8

gcc SVN确实有一种结构重组优化(-fipa-struct-reorg),但它需要整个程序的分析,目前并不是很强大。


1
10年后的标准gcc(版本7.2,由Ubuntu 17.10打包)在手册页面中没有记录此选项。但是,奇怪的是,gcc可执行文件可以识别该选项字符串。 - FooF

3

并不是说这是一个好主意,但你可以编写依赖于结构体成员顺序的代码。例如,作为一种hack,人们经常将指向结构体的指针强制转换为其中某个字段的类型,然后使用指针算术运算来访问该字段。对我来说,这是一个非常危险的想法,但我已经看到它被使用了,特别是在C++中,当一个变量被声明为私有时,可以通过这种方式使其在第三方库的类中公开访问而不被公开封装。重新排序成员会完全破坏这种实现。


我相信Linux内核可以为链表执行此操作。 - Domingo Ignacio

2

因为像你提到的对齐问题,C编译器不会自动精确地打包结构体。在x86上,没有按字边界(大多数CPU上的32位)访问会带来沉重的惩罚,并在RISC架构上引起致命陷阱。


1
我不是在说要摆脱缓冲区,我是在说将所有的长整型/指针放在一起,然后将所有的短整型放在一起,再将所有的字符放在一起等等,这样你只会在末尾失去一些空间。 - Alex Gartrell
@Alex,出错了。由于您的字符必须填充相同的数量,因此您将浪费相同的空间。无论是在空间还是性能方面,您都不会受益。 - Serafina Brocious
哦,是的,这会导致二进制格式出现问题,正如Cody所证实的那样。此外,ANSI保证结构元素偏移量必须按递增顺序排列。 - Alex M
@Cody,所有的空间将在最后浪费,并且少于或等于常规的“按需分配”的方法。例如:结构体blah{ char a; short b; char c; };结构体blah2{ short b; char a, c; };blah比blah2浪费了2个额外字节。 - Alex Gartrell
1
通过正确排列结构体,您不会失去填充的任何好处。使用 short、char、char,您可以实现零填充,但所有元素都落在正确的偏移量上。通常情况下,这样做不会丧失任何速度,因为它们落在自然边界上。 - Alex Gartrell
显示剩余5条评论


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接