假设我有这样的结构:
1 2 3 4 5 6 7 8
| struct MyStruct
{
uint8_t var0;
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
}; |
这可能会浪费一堆(而不是一吨)的空间。这是因为uint32_t变量必须对齐。
实际上(在对结构进行了调整以便它可以实际使用uint32_t变量之后),它可能看起来像这样:
1 2 3 4 5 6 7 8 9
| struct MyStruct
{
uint8_t var0;
uint8_t unused[3]; //3 bytes of wasted space
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
}; |
更有效的结构是:
1 2 3 4 5 6 7 8
| struct MyStruct
{
uint8_t var0;
uint8_t var2;
uint8_t var3;
uint8_t var4;
uint32_t var1;
}; |
现在,问题是:
为什么编译器(按标准)禁止重新排序结构?
如果结构被重新排序,我看不出有什么方法可以让你的脚自己被射中。
- 首先,重新排序需要是"确定性的",并作为工具链ABI的一部分发布。
- 序列化?您将一个结构流式输出到一个文件,然后重新编译,并尝试将其流式返回。如果允许编译器对成员重新排序,结果会是什么?
- @iInspectable-总之,这很危险(不使用特定于平台的打包语用等)
- @据我所知,已经可以更改(除非包装好,标准不支持)
- 我不知道为什么标准明确禁止重新排序。但即使它没有编译器,编译器仍然无法做到这一点,因为它需要编译器无所不知。(请记住,通过指向兼容但不相同类型的结构的指针访问结构是合法的。)
- 如果这个结构是我的协议头结构,我就命中注定了。
- @olivercharlesworth:Alignment是语言规范的一部分(参见例如Alignas)。当然,没有指定二进制表示,因此必须确保所有竞争方都同意。这也是一个可以解决的挑战。
- IsCuthTe-没有注意到这也被标记为C++!但这在概念上与语用和其他魔术一样——重点是在你关心位布局的情况下,你已经明确地控制了它(这可能不适用于操作问题,这似乎是关于你(程序员)不关心布局的情况)。
- 旁注:一般来说,从大到小(按大小)排序的成员比从小到大排序的成员要好。
- 看起来GCC尝试过但失败了:stackoverflow.com/questions/14671253/…
- @ MarcinJ?泽耶夫斯基博士:很好的发现。或许值得这样回答。
- 请注意,GCC和CLAN支持-WPADAD警告,这将告诉您,如果结构在中间有额外填充。
- EricRaymond说,在C结构封装的丢失艺术中,"C是一种最初设计用于编写操作系统和硬件附近的其他代码的语言。自动重新排序会干扰系统程序员布局结构的能力,这些结构与内存映射设备控制块的字节级和位级布局完全匹配。"
- 值得注意的是,在C++中,编译器可以在一定程度上重新排序成员。
Why is the compiler forbidden (by the standard) from reordering the struct?
基本原因是:为了与C兼容。
记住,C最初是一种高级汇编语言。在C中,通过将字节重新解释为特定的struct来查看内存(网络包,…)。
这导致多个功能依赖于此属性:
后者非常广泛,并且完全阻止对大多数struct或class的数据成员重新排序。
注意,标准确实允许一些重新排序:由于C没有访问控制的概念,C++指定了两个具有不同访问控制说明符的数据成员的相对顺序。
据我所知,没有编译器试图利用它,但理论上他们可以。
在C++之外,诸如Ru锈这样的语言允许编译器重新排序字段,而主RISC编译器(RUSTC)默认是这样的。只有历史决定和强烈的向后兼容性要求才能阻止C++这样做。
- 这里有一些好的观点。提醒我,如果更改-std标志的值,编译运行之间的顺序可能会有所不同;
I don't see any way you could shoot your self in the foot, if the struct was reordered.
真的?如果允许这样做,库/模块之间的通信,即使是在同一个过程中,默认情况下也是非常危险的。
"宇宙"论据
我们必须知道我们的结构是按照我们要求的方式定义的。填充未指定已经够糟糕了!幸运的是,您可以在需要时控制它。
好吧,理论上,一种新的语言可以被做成这样,类似地,成员可以重新排序,除非给出了一些属性。毕竟,我们不应该在对象上做内存级别魔术,所以如果只使用C++习语,默认情况下是安全的。
但这不是我们生活的现实。
"宇宙外"论点
用你的话说,如果"每次都使用相同的重新排序",你就可以确保事情的安全。该语言必须明确说明如何对成员进行排序。标准编写起来很复杂,理解起来很复杂,实现起来也很复杂。
只需保证顺序和代码中的顺序一样,并将这些决定留给程序员就容易多了。记住,这些规则源于旧的C,而旧的C为程序员提供了动力。
您已经在您的问题中展示了如何通过简单的代码更改使结构填充有效。不需要在语言级别增加任何复杂性来为您完成这项工作。
- 如果每次都使用相同的重新排序,则不会……
- @达特鲁比克:你如何每次使用相同的顺序来执行每个编译器的每次运行?哦,对了,把它留作程序员写的lol
- 在同一进程中,库/模块之间的通信将是极其危险的。
- @奥利弗卡尔斯沃思:是的,很好的观点-被偷了:)
- @在他的例子中,它很容易制造出来。我们可以很容易地想象一些例子,在这些例子中,很难做到这一点,特别是在代码中,这意味着可以移植到具有不同对象大小和对齐要求的平台上。它还可能使代码更难理解和记录,因为您不能按函数对成员进行分组。
- @戴维施瓦茨:同意。我想我支持添加一个属性来允许重新排序,但是默认情况下它不能打开。
- @Darthrubik你必须描述一个算法,给定序列的大小和对齐元素,确定地转换它们的顺序,所以最终对象的大小将是最小的。这闻起来像是包装问题,可能不难。
- @作为ABI的一部分,Revolver ocelot平台可以指定一个简单的、确定性的重新排序方案,该方案以最低的成本获得显著的打包效益。例如,只需按大小(第一个最大)对对象进行稳定排序就可以了。
- @Davidschwartz将进一步推迟这个问题:"为什么编译器不重新排序元素,在这种情况下,更好的顺序是显而易见的"。不过,允许compler对成员重新排序的特定属性的想法是好的。
- 该语言不必为模块间的兼容性指定填充或顺序;这与ABI处理函数调用非常类似。
- 似乎更适合像"未对齐的结构"这样的警告,而不是自动的。
- @左轮手枪:这将如何进一步推迟这个问题?他说的是包装,而不是遗漏。当你打包的时候,"为什么不打包?"不再存在。它不会延迟,它会消失。与协议相关的问题可以通过关键字(如unpacked struct { ... }或类似关键字)解决。对于现代C++,当然需要相反的关键字,因为向后兼容。
- "提姆?"因为没有提到包装。我们讨论的是结构元素的自动重新排序,以最小化因相邻成员的大小/对齐要求而产生的填充量。完全重新排序可能不可能/feasmile进行计算(它看起来太类似于打包问题),不完全重新排序只会将问题标题从"为什么编译器不优化"更改为"为什么编译器不更好优化"。
- @左轮手枪:我知道你在说什么。这和包装问题有什么相似之处?只要做一个稳定的分类,就行了。这可以用计数排序在线性时间内完成。
- "提姆?"按什么标准排序?如何考虑到16字节对齐的1字节成员和不受任何对齐限制的16字节成员都可以存在?(以及任何其他尺寸/对齐组合)
- @左轮手枪:按对齐方式排序。前者不能存在,因为16字节对齐立即意味着至少16字节大小(因为数组)。即使假设它们存在(例如,对于某种高级优化,尽管随后会遇到一些与OOP相关的问题),也可以使用孔填充算法。我以前实施过,这不是火箭科学。对于后一种情况,这很容易:您将它精确地放在没有对齐限制的1字节成员所在的位置(唯一的区别是为它分配了15个字节的空间)。
标准之所以保证分配顺序,仅仅是因为结构可以表示特定的内存布局,例如数据协议或硬件寄存器集合。例如,程序员和编译器都不能自由地重新安排TPC/IP协议中字节的顺序,也不能自由地重新安排微控制器的硬件寄存器。
如果订单没有得到保证,EDCOX1(0)将仅仅是抽象的数据容器(类似于C++向量),我们不能承担太多,除非它们以某种方式包含了我们放入的数据。在进行任何形式的低级编程时,这将使它们实际上更无用。
- 但这是否违背了"不要为你不使用的东西付出代价"的基本准则?当然,这种情况在少数情况下,减少内存消耗和减少内存带宽使用的好处并不微乎其微。对于避免重新排序的关键字来说,这是一个很好的参数,但对于从不重新排序则不是。
- @Davidschwartz好吧…结构是一个半心半意的尝试,以适应每个人,硬件程序员和CPU的一致性。如果编译器不自动处理结构填充,它们将更加有用和可移植。我想有两种不同的数据类型:"strict struct"和"i dont care struct"会非常方便。有点像uint8_t和uint_fast8_t。
- 所以可能是因为有时您需要保留其顺序的结构,而且似乎从来没有足够的理由在标准中指定两种不同类型的结构?
- @如今,如果你真的需要更紧的内存使用,那么你几乎肯定是在一个嵌入式平台上工作,因为这种级别的内存使用在PC上已经有几十年没有被认真考虑过了。如果你正在研究嵌入式的东西,那么你就不可避免地知道这些问题,并且能够自己解决——如果你不知道,那么现在是你解决问题的时候了。因此,唯一能帮上忙的人是那些能力不强的新手,他们所面临的挑战,我认为这是一种很小的啤酒。
- @Graham结构成员排序和填充的问题不是内存使用问题,而是它可能导致结构不复制它应该表示的预期数据协议/硬件寄存器。具有固定顺序和无填充的结构将帮助所有人。今天,我们必须求助于非标准的C,如#pragma pack等,才能实现这一目标。
- @我不会说这是少数族裔的案子。我处理过的大多数C程序员都假设结构具有固定的内存布局,并充分利用它——最重要的是,将序列化替换为简单的转换。例如,原始魔兽制作了一个保存游戏,通过一次写入将整个游戏状态从内存复制到文件(反之亦然)。我们在这里讨论的是C——它是一种改进了可移植性的汇编语言。但是,即使在C(托管语言)中,这个规则在默认情况下也适用——简化互操作。
- @伦丁记忆的使用是手术的角度。我同意无声填充结构对于协议也是一个问题,尽管对于硬件寄存器来说这不太可能是一个问题,因为它们通常是处理器字长的大小,并且字边界对齐。但同样,这是在嵌入式代码(或者至少是驱动程序代码)中完成的,对于初学者来说,这不是一项任务。我可以看到它的好处,但同样的,任何可能需要它的人也会知道如何自己排序。
如果结构由其他编译器或其他语言生成的任何其他低级代码读取,则编译器应保持其成员的顺序。假设您正在创建一个操作系统,并且您决定用C编写它的一部分,用汇编编写它的一部分。您可以定义以下结构:
1 2 3 4 5
| struct keyboard_input
{
uint8_t modifiers;
uint32_t scancode;
} |
您将它传递给一个程序集例程,在那里您需要手动指定结构的内存布局。您希望能够在4字节对齐的系统上编写以下代码。
1 2 3
| ; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4] |
现在假设编译器将以实现定义的方式更改结构中成员的顺序,这意味着根据您使用的编译器和传递给它的标志,您可以以al中scancode成员的第一个字节结束,也可以以modifiers成员结束。
当然,问题不只是减少到与汇编例程的低级接口,而且如果使用不同编译器构建的库相互调用(例如,使用Windows API构建mingw程序),问题也会出现。
因此,语言只是强迫您考虑结构布局。
- 这没有道理。这些标准不需要足够的保证。例如,它允许根据您使用的编译器和传递给它的标志更改填充。所以这并不能解释为什么特别禁止重新订购。
- 因此,系统与4字节对齐。它将是一个系统,其中所有数据结构的成员都被填充以从一个4字节的边界开始,这在32位系统中相当常见。
- @Davidschwartz是的,但这并不重要——填充是系统的一部分,当你编写程序集时,你已经在对系统进行编码了。也不要认为没有太多人对自动包装感到恼火;)
记住,不仅自动重新排序元素以改进打包可以损害特定的内存布局或二进制序列化,而且程序员可能已经仔细选择了属性的顺序,以使经常使用的成员的缓存位置对很少访问的成员有利。
你也引用C++,所以我会给你一个实际的原因,这是不可能发生的。
鉴于class和struct之间没有区别,考虑:
[cc lang="cpp"]class myclass{字符串S;另一对象B;myclass():s"你好
- [C++11:9.2/14 ]:具有相同访问控制(第11条)的(非结合)类的非静态数据成员被分配,以便以后成员在类对象中具有更高的地址。(我的重点)
- 当然,初始化顺序与物理布局无关。
- @杰里米:不确定。这实际上是一个直接后果,正如我在回答中所解释的(如果有点不清楚,我会尽力澄清)。
- 请澄清。
- 您所说的"编译器不是严格要求的,不需要在内存中对它们重新排序(我能说什么)"是什么意思?你能澄清一下吗?
丹尼斯·里奇设计的语言定义了结构的语义,而不是行为,而是内存布局。如果结构s在偏移量x处具有类型t的成员m,则m.s的行为定义为获取s的地址,向其添加x字节,将其解释为指向t的指针,并将由此标识的存储解释为左值。写入结构成员将更改其关联存储的内容,更改成员存储的内容将更改成员的值。代码可以自由地使用各种各样的方法来操作与结构成员相关联的存储,并且可以根据对该存储的操作来定义语义。
代码操作与结构关联的存储的有效方法之一是使用memcpy()将一个结构的任意部分复制到另一个结构的相应部分,或者使用memset()清除结构的任意部分。由于结构成员是按顺序排列的,因此可以使用单个memcpy()或memset()调用复制或清除一系列成员。
标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储的要求,或对存储的更改影响成员值的要求,从而使结构布局的保证不如里奇的语言有用。尽管如此,仍然保留了使用memcpy()和memset()的能力,并且保留这种能力需要保持结构元素的顺序性。
假设这个结构布局实际上是一个"通过线"接收到的内存序列,比如一个以太网包。如果编译器重新对齐以提高效率,那么您将不得不以所需的顺序执行大量的工作,而不是仅仅使用一个结构,它以正确的顺序和位置拥有所有正确的字节。
- 总之,这是危险的(不使用特定于平台的打包实用程序等)。
- @Olivercharlesworth是的,但是如果你使用的是一个嵌入式处理器,它的RAM/ROM有限,这可能是唯一的出路!
- 同意。但关键是在那个场景中,您应该已经显式地控制结构布局了。