可能重复:
Why doesn't GCC optimize structs?
Why doesn't C++ make the structure tighter?
在32位x86计算机上考虑以下示例:
由于对齐约束,以下结构
struct s1 {
char a;
int b;
char c;
char d;
char e;
}
如果成员按照重新排序,则可以更有效地表示内存(12对8字节)
struct s2 {
int b;
char a;
char c;
char d;
char e;
}
我知道C / C ++编译器不允许这样做。我的问题是为什么语言是这样设计的。毕竟,我们最终可能会浪费大量的内存,struct_ref->b
之类的引用也不会关心差异。
编辑:谢谢大家的非常有用的答案。您解释为什么由于语言的设计方式,重新排列不起作用。然而,它让我想到:如果重新排列是语言的一部分,这些论点是否仍然有效?假设有一些指定的重新排列规则,我们至少需要这个规则
我一个接一个地论证你的论点:
低级数据映射,“最少惊喜的元素”:只需自己编写一个紧凑的结构(比如@ Perry的回答)并且没有任何改变(要求1) 。如果由于一些奇怪的原因,你想要内部填充,你可以使用虚拟变量手动插入它,和/或可能有关键字/指令。
编译器差异:要求3消除了这种担忧。实际上,从@David Heffernan的评论来看,我们今天似乎遇到了这个问题,因为不同的编译器填充方式不同?
优化:重新排序的重点是(内存)优化。我在这看到很多潜力。我们可能无法一起删除填充,但我没有看到重新排序如何以任何方式限制优化。
类型转换:在我看来,这是最大的问题。不过,应该有办法解决这个问题。由于规则是用语言修复的,因此编译器能够弄清楚成员的重新排序方式,并做出相应的反应。如上所述,在您想要完全控制的情况下,始终可以防止重新排序。此外,要求2确保类型安全代码永远不会中断。
我认为这样一条规则有意义的原因是因为我觉得按结构内容而不是按类型对结构成员进行分组更为自然。当我有很多内部结构时,编译器也更容易选择最适合我的顺序。最佳布局甚至可能是我无法以类型安全的方式表达的布局。另一方面,它似乎使语言更复杂,这当然是一个缺点。
请注意,我不是在谈论改变语言 - 只有它可以(/应该)设计不同。
我知道我的问题是假设的,但我认为讨论提供了对机器和语言设计较低层次的更深入的了解。
我在这里很新,所以我不知道是否应该为此产生一个新问题。请告诉我是否是这种情况。
答案 0 :(得分:69)
C编译器无法自动对字段重新排序有多种原因:
C编译器不知道struct
是否代表当前编译单元之外的对象的内存结构(例如:外部库,光盘上的文件,网络数据,CPU页表) ,...)。在这种情况下,数据的二进制结构也在编译器无法访问的位置定义,因此重新排序struct
字段将创建与其他定义不一致的数据类型。例如,header of a file in a ZIP file包含多个未对齐的32位字段。重新排序字段将使C代码无法直接读取或写入标头(假设ZIP实现想直接访问数据):
struct __attribute__((__packed__)) LocalFileHeader {
uint32_t signature;
uint16_t minVersion, flag, method, modTime, modDate;
uint32_t crc32, compressedSize, uncompressedSize;
uint16_t nameLength, extraLength;
};
packed
属性阻止编译器根据字段的自然对齐来对齐字段,它与字段排序问题无关。可以对LocalFileHeader
的字段进行重新排序,以使结构具有最小尺寸并使所有字段与其自然对齐对齐。但是,编译器无法选择重新排序字段,因为它不知道该结构实际上是由ZIP文件规范定义的。
C是一种不安全的语言。 C编译器不知道是否将通过与编译器看到的类型不同的类型访问数据,例如:
struct S {
char a;
int b;
char c;
};
struct S_head {
char a;
};
struct S_ext {
char a;
int b;
char c;
int d;
char e;
};
struct S s;
struct S_head *head = (struct S_head*)&s;
fn1(head);
struct S_ext ext;
struct S *sp = (struct S*)&ext;
fn2(sp);
这是广泛使用的低级编程模式,特别是如果标头包含位于标题之外的数据的类型ID。
如果struct
类型嵌入了另一个struct
类型,则无法内联struct
:
struct S {
char a;
int b;
char c, d, e;
};
struct T {
char a;
struct S s; // Cannot inline S into T, 's' has to be compact in memory
char b;
};
这也意味着将某些字段从S
移动到单独的结构会禁用某些优化:
// Cannot fully optimize S
struct BC { int b; char c; };
struct S {
char a;
struct BC bc;
char d, e;
};
由于大多数C编译器都在优化编译器,因此重新排序struct字段需要实现新的优化。值得怀疑的是,这些优化是否能够比程序员能够编写的更好。手工设计数据结构比其他编译器任务(例如寄存器分配,函数内联,常量折叠,将switch语句转换为二进制搜索等)耗时更少,因此可以获得的好处允许编译器优化数据结构似乎比传统的编译器优化更不明显。
答案 1 :(得分:28)
C旨在使用高级语言编写非便携式硬件和格式相关代码成为可能。程序员背后的结构内容重新排列会破坏这种能力。
观察NetBSD的ip.h中的实际代码:
/*
* Structure of an internet header, naked of options.
*/
struct ip {
#if BYTE_ORDER == LITTLE_ENDIAN
unsigned int ip_hl:4, /* header length */
ip_v:4; /* version */
#endif
#if BYTE_ORDER == BIG_ENDIAN
unsigned int ip_v:4, /* version */
ip_hl:4; /* header length */
#endif
u_int8_t ip_tos; /* type of service */
u_int16_t ip_len; /* total length */
u_int16_t ip_id; /* identification */
u_int16_t ip_off; /* fragment offset field */
u_int8_t ip_ttl; /* time to live */
u_int8_t ip_p; /* protocol */
u_int16_t ip_sum; /* checksum */
struct in_addr ip_src, ip_dst; /* source and dest address */
} __packed;
该结构在布局上与IP数据报的标题相同。它用于直接解释由以太网控制器作为IP数据报报头的内存blob。想象一下,如果编译器任意重新安排了作者下面的内容 - 这将是一场灾难。
是的,它不是精确可移植的(甚至还有通过__packed
宏给出的非便携式gcc指令),但这不是重点。 C特别是设计,可以编写用于驱动硬件的非便携式高级代码。这就是它在生活中的作用。
答案 2 :(得分:11)
C [和C ++]被认为是系统编程语言,因此它们通过指针提供对硬件的低级访问,例如存储器。程序员可以访问数据块并将其转换为结构并[轻松]访问各种成员。
另一个例子是类似下面的结构,它存储可变大小的数据。
struct {
uint32_t data_size;
uint8_t data[1]; // this has to be the last member
} _vv_a;
答案 3 :(得分:10)
不是WG14的成员,我不能说明确,但我有自己的想法:
这会违反最不惊讶的原则 - 可能有一个很好的理由,为什么我要按特定的顺序排列我的元素,无论它是否是最节省空间的,我会不希望编译器重新排列这些元素;
它有可能打破一些非常重要的现有代码 - 那里有很多遗留代码依赖于结构的地址与第一个成员的地址相同的东西(看到很多经典的MacOS代码都做出了这个假设);
C99 Rationale直接解决第二点(“现有代码很重要,现有实现不是”)并间接解决第一点(“信任程序员”)。
答案 4 :(得分:9)
它会改变指针操作的语义,以重新排序结构成员。如果您关心压缩内存表示,那么作为程序员,您有责任了解目标体系结构,并相应地组织结构。
答案 5 :(得分:6)
如果您正在向/从C结构读取/写入二进制数据,那么struct
成员的重新排序将是一场灾难。例如,没有实际的方法可以从缓冲区实际填充结构。
答案 6 :(得分:5)
结构用于表示最低级别的物理硬件。因此,编译器无法移动一轮以适应该级别。
然而,让#pragma让编译器重新安排纯粹基于内存的结构只是程序内部使用是不合理的。但是我不知道这样的野兽(但这并不意味着蹲下 - 我与C / C ++失去联系)
答案 7 :(得分:3)
请记住,变量声明(例如结构)被设计为变量的“公共”表示。它不仅被您的编译器使用,而且也可用于表示该数据类型的其他编译器。它可能会以.h文件结尾。因此,如果编译器要按照结构中成员的组织方式采取自由,那么所有编译器必须能够遵循相同的规则。否则,正如已经提到的,指针算法将在不同的编译器之间混淆。
答案 8 :(得分:2)
这是我到目前为止没有看到的原因 - 如果没有标准的重新排列规则,它会破坏源文件之间的兼容性。
假设在头文件中定义了一个结构,并在两个文件中使用
两个文件分别编译,然后链接。编译可能在不同的时间(可能只触及一次,因此必须重新编译),可能在不同的计算机上(如果文件在网络驱动器上)或甚至不同的编译器版本。
如果一次,编译器将决定重新排序,而另一方面则不会,两个文件将不同意字段的位置。
例如,考虑stat
系统调用和struct stat
当您安装Linux(例如)时,您会获得libC,其中包含stat
,这是某人有时编译的。
然后使用编译器使用优化标志编译应用程序,并期望两者同意结构的布局。
答案 9 :(得分:1)
您的案例非常具体,因为它需要重新排序struct
的第一个元素。这是不可能的,因为struct
中首先定义的元素必须始终位于偏移0
。如果允许的话,很多(伪造的)代码都会破坏。
更常见的是,位于同一个更大对象内的子对象指针必须始终允许指针比较。我可以想象,如果您反转订单,使用此功能的某些代码会中断。并且为了进行比较,编译器在定义时的知识无济于事:指向子对象的指针没有“标记”它所属的更大的对象。当像这样传递给另一个函数时,可能的上下文的所有信息都将丢失。
答案 10 :(得分:0)
假设你有一个标题a.h with
struct s1 {
char a;
int b;
char c;
char d;
char e;
}
这是一个单独的库的一部分(其中只有未知编译器编译的已编译二进制文件),并且您希望使用此结构与此库进行通信,
如果允许编译器以任何方式重新排序成员这将是不可能的,因为客户端编译器不知道是否使用结构 - 是或优化(然后b
在前面或后面)或甚至完全填充每个成员以4个字节的间隔对齐
要解决此问题,您可以为压缩定义一个确定性算法,但这需要所有编译器实现它,并且算法是一个很好的算法(就效率而言)。与重新排序相比,更容易就填充规则达成一致
很容易添加一个#pragma
,禁止优化,当你需要特定结构的布局时,正是你需要的,这是没有问题的