为什么C编译器不能重新排列结构成员来消除alignment填充?
可能重复:
为什么GCC不优化结构?
为什么C ++不使结构更紧密?
考虑在32位x86机器上的以下示例:
由于alignment约束,下面的结构
struct s1 { char a; int b; char c; char d; char e; }
可以更有效地代表内存(12比8字节),如果成员被重新sorting的话
struct s2 { int b; char a; char c; char d; char e; }
我知道C / C ++编译器不允许这样做。 我的问题是为什么语言是这样devise的。 毕竟,我们最终可能会浪费大量的内存, struct_ref->b
引用不会关心这个区别。
编辑 :谢谢大家的非常有用的答案。 你很好地解释了为什么重新安排因为devise语言的方式而不起作用。 但是,这让我想:如果重新安排是语言的一部分,这些论据是否仍然会成立? 假设有一些特定的重排规则,我们至less要求这样做
- 我们应该只在实际需要时重新组织结构(如果结构已经“紧”,不要做任何事情)
- 该规则仅查看结构的定义,而不是内部结构。 这确保了一个结构types具有相同的布局,而不pipe它是否在另一个结构中是内部的
- 给定结构的编译内存布局是可预测的,因为它的定义(即规则是固定的)
我一个接一个地说我的理由:
-
低级数据映射,“最不可思议的元素” :只要你自己写一个简洁的结构(比如@Perry的答案),没有什么变化(需求1)。 如果出于某种奇怪的原因,您希望内部填充在那里,您可以使用虚拟variables手动插入它,和/或可能有关键字/指令。
-
编译器差异 :要求3消除了这个问题。 其实,从@David Heffernan的评论看来,我们今天似乎有这个问题,因为不同的编译器有不同的填充?
-
优化 :重新sorting的整个内容是(内存)优化。 我在这里看到很多潜力。 我们可能无法一起移除填充,但是我不知道重新sorting如何以任何方式限制优化。
-
types铸造 :在我看来,这是最大的问题。 不过,应该有办法解决这个问题。 由于规则在语言中是固定的,因此编译器能够计算出成员如何重新sorting,并作出相应的反应。 如上所述,在想要完全控制的情况下,始终可以防止重新sorting。 另外,需求2确保types安全的代码永远不会中断。
我认为这样一个规则是有道理的,因为我觉得把结构成员按其内容分类比按types更自然。 当我有很多内部结构时,编译器select最好的顺序比我更容易。 最佳布局甚至可能是我无法以types安全的方式expression的。 另一方面,它似乎会使语言更复杂,这当然是一个缺点。
请注意,我不是在谈论改变语言 – 只有当它可以(/应该)的devise不同。
我知道我的问题是假设的,但是我认为这个讨论在机器和语言devise的较低层面上提供了更深入的见解。
我在这里很新,所以我不知道是否应该为此提出一个新的问题。 请告诉我,如果是这样的话。
C编译器无法自动重新sorting字段有多种原因:
-
C编译器不知道
struct
是否代表当前编译单元以外的对象(例如:外部库,光盘上的文件,networking数据,CPU页表等)的内存结构。 在这种情况下,数据的二进制结构也被定义在编译器无法访问的地方,所以重新sortingstruct
字段将会创build一个与其他定义不一致的数据types。 例如, ZIP文件中的文件头包含多个未alignment的32位字段。 对字段进行重新sorting将使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
属性阻止编译器按照自然alignment方式alignment字段,并且与字段sorting问题无关。 可以对LocalFileHeader
的字段进行重新sorting,以使结构既具有最小的大小,又具有与自然alignmentalignment的所有字段。 但是,编译器不能select对字段进行重新sorting,因为它不知道该结构实际上是由ZIP文件规范定义的。 -
C是一种不安全的语言。 C编译器不知道数据是否会通过与编译器所看到的types不同的types来访问,例如:
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);
这是一个广泛使用的低级编程模式,特别是如果标题包含位于标题之外的数据typesID。
-
如果一个
struct
types被embedded到另一个struct
types中,则不可能内联内部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编译器都在优化编译器,重新sortingstruct字段将需要实现新的优化。 这些优化是否能够比程序员能够写得更好是值得怀疑的。 手工devise数据结构比其他编译器任务(如寄存器分配,函数内联,常量折叠,将开关语句转换为二分search等) 花费的时间要less得多。因此,通过允许编译器优化数据结构似乎不如传统的编译器优化有形。
C的devise目的是使得用高级语言编写不可移植的硬件和格式相关的代码成为可能。 程序员背后的结构内容重新排列会破坏这个能力。
从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数据报头。 想象一下,如果编译器任意地将作者的内容重新排列出来 – 这将是一场灾难。
是的,它不是可移植的(甚至有通过__packed
macros给出的不可移植的gcc指令),但这不是重点。 C专门devise用于编写用于驱动硬件的非便携式高级代码。 这是它在生活中的function。
不是WG14的成员,我不能说任何明确的,但我有我自己的想法:
-
这违反了最less让人吃惊的原则 – 为什么我要按照特定的顺序排列我的元素,而不pipe它是否是最节省空间的,我可能会有一个很好的理由,我不希望编译器重新排列那些元素;
-
它有可能会破坏现有代码的不平凡数量 – 有很多遗留代码依赖于结构的地址与第一个成员的地址相同的东西(看到很多经典的MacOS做出这个假设的代码);
C99原理直接解决了第二点(“现有代码很重要,现有的实现不是”),并间接地解决了第一点(“信任程序员”)。
C [和C ++]被认为是系统编程语言,因此它们通过指针提供对硬件的低级访问,例如存储器。 程序员可以访问一个数据块,并将其转换为一个结构,并轻松访问各种成员。
另一个例子是像下面这样的结构,它存储可变大小的数据。
struct { uint32_t data_size; uint8_t data[1]; // this has to be the last member } _vv_a;
它会改变指针操作的语义来重新sorting结构成员。 如果你关心紧凑的内存表示,那么作为一个程序员你有责任了解你的目标体系结构,并相应地组织你的结构。
如果您正在从C结构中读取/写入二进制数据,对struct
成员进行重新sorting将是一场灾难。 例如,没有实际的方法来实际上从缓冲区填充结构。
结构被用来表示最底层的物理硬件。 因此,编译器无法将事情调整到适合的水平。
然而,编译#pragma让编译器重新安排纯粹的基于内存的结构,而这些结构只在程序内部使用,这并不是不合理的。 不过,我不知道这样的野兽(但这并不意味着蹲下 – 我与C / C ++脱节)
请记住,variables声明(如结构体)被devise为variables的“公共”表示forms。 它不仅被你的编译器使用,而且也被其他编译器用来表示这个数据types。 它可能会以.h文件结束。 因此,如果一个编译器将采取一种结构中的成员组织方式的自由,那么所有的编译器必须能够遵循相同的规则。 否则,如前所述,指针运算会在不同的编译器之间混淆。
这是我迄今为止没有看到的一个原因 – 没有标准的重新排列规则,它会破坏源文件之间的兼容性。
假设在头文件中定义了一个结构体,并在两个文件中使用。
这两个文件都是分开编译的,后来被链接。 编译可能在不同的时间(也许你碰到一个,所以它必须重新编译),可能在不同的计算机上(如果文件在networking驱动器上)甚至不同的编译器版本。
如果有一次,编译器会决定重新sorting,而另一个则不会,这两个文件将不同意字段的位置。
作为一个例子,考虑stat
系统调用和struct stat
。
当你安装Linux(例如)时,你会得到libC,其中包括某人某时编译的stat
。
然后用你的编译器编译一个应用程序,用你的优化标志,并且期望两者同意结构的布局。
你的情况是非常具体的,因为它需要结构的第一个元素被重新sorting。 这是不可能的,因为在struct
首先定义的元素必须始终在偏移0
。 如果允许的话,很多(假)代码将会被破坏。
更一般地说,居于同一个较大对象内的子对象的指针必须始终允许指针比较。 我可以想象,如果您反转订单,使用此function的某些代码将会中断。 而对于这种比较,编译器在定义点的知识将无济于事:指向子对象的指针没有“标记”来确定它所属的更大的对象。 当像这样传递给另一个函数时,可能上下文的所有信息都将丢失。
假设你有一个头啊
struct s1 { char a; int b; char c; char d; char e; }
这是一个单独的库的一部分(你只有编译的二进制文件由一个未知的编译器编译),你希望使用这个结构体与这个库进行通信,
如果允许编译器以任何方式对成员进行重新sorting, 这将是不可能的,因为客户端编译器不知道是按原样使用结构还是优化(然后在前面或后面)或甚至以每4个字节间隔排列的每个成员完全填充
为了解决这个问题,你可以定义一个确定性的压缩algorithm,但是这需要所有的编译器来实现,algorithm是一个很好的algorithm(效率)。 与重新sorting相比,只需要就填充规则达成一致就比较容易
很容易添加一个#pragma
,当你需要一个特定的结构的布局时,禁止优化,正是你所需要的,所以这是没有问题的