编译器重新排序结构
Struct Reordering by compiler
假设我有这样一个结构:
struct MyStruct
{
uint8_t var0;
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
};
这可能会浪费一堆(不是一吨)space。这是因为 uint32_t
变量需要对齐。
实际上(在对齐结构以便它可以实际使用 uint32_t
变量之后)它可能看起来像这样:
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;
};
一个更有效的结构是:
struct MyStruct
{
uint8_t var0;
uint8_t var2;
uint8_t var3;
uint8_t var4;
uint32_t var1;
};
现在,问题是:
为什么(标准)禁止编译器对结构重新排序?
如果结构被重新排序,我看不出任何搬起石头砸自己脚的方法。
想象一下,这个结构布局实际上是一个接收到的内存序列 'over the wire',比如说一个以太网数据包。如果编译器重新调整事情以提高效率,那么您将不得不做大量工作以按要求的顺序提取字节,而不是仅仅使用具有正确顺序和位置的所有正确字节的结构。
I don't see any way you could shoot your self in the foot, if the struct was reordered.
真的吗?如果允许这样做,默认情况下 libraries/modules 之间的通信即使在同一进程中也会非常危险。
"In universe" 参数
我们必须能够知道我们的结构是按照我们要求的方式定义的。未指定填充已经够糟糕了!幸运的是,您可以在需要时控制它。
好的,理论上,可以创建一种新语言,类似地,成员可以重新排序除非给出某些属性。毕竟,我们不应该对对象进行内存级魔法,所以如果只使用 C++ 习惯用法,默认情况下你是安全的。
但这不是我们生活的实际现实。
"Out of universe" 参数
如果用您的话说,"the same reorder was used every time",您可以使事情变得安全。该语言必须明确说明成员的排序方式。这在标准中编写起来很复杂,理解起来也很复杂,实施起来也很复杂。
只需保证顺序与代码中的顺序一致,然后将这些决定留给程序员,就容易多了。请记住,这些规则起源于旧 C,旧 C 赋予 程序员 权力。
您已经在您的问题中展示了通过简单的代码更改使结构填充高效是多么容易。无需在语言级别增加任何复杂性即可为您执行此操作。
标准保证分配顺序只是因为结构可能表示特定的内存布局,例如数据协议或硬件寄存器集合。例如,程序员和编译器都不能随意重新安排 TPC/IP 协议中的字节顺序,或者微控制器的硬件寄存器。
如果不能保证顺序,structs
将仅仅是抽象的数据容器(类似于 C++ 向量),我们不能对其进行过多假设,只是它们以某种方式包含我们放入其中的数据他们。在进行任何形式的低级编程时,这会使它们变得更加无用。
The compiler should keep the order of its members in the case the structures are read by any other low-level code produced by another compiler or another language. Say you were creating an operating system, and you decide to write part of it in C, and part of it in assembly. You could define the following structure:
struct keyboard_input
{
uint8_t modifiers;
uint32_t scancode;
}
You pass this to an assembly routine, where you need to manually specify the memory layout of the structure. You would expect to be able to write the following code on a system with 4-byte alignment.
; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]
Now say the compiler would change the order of the members in the structure in an implementation defined way, this would mean that depending on the compiler you use and the flags you pass to it, you could either end up with the first byte of the scancode member in al, or with the modifiers member.
Of course the problem is not just reduced to low-level interfaces with assembly routines, but would also appear if libraries built with different compilers would call each other (e.g. building a program with mingw using the windows API).
Because of this, the language just forces you to think about the structure layout.
请记住,不仅自动重新排序元素以改进打包可能会损害特定的内存布局或二进制序列化,而且属性的顺序可能已被程序员仔细选择以有利于缓存局部性经常使用的成员与很少访问的成员。
Why is the compiler forbidden (by the standard) from reordering the struct?
基本原因是:为了兼容C.
请记住,C 最初是一种高级汇编语言。在 C 中,通过将字节重新解释为特定的 struct
.
来查看内存(网络数据包,...)是很常见的
这导致了依赖此 属性 的多个功能:
C 保证 struct
的地址和它的第一个数据成员的地址是相同的,所以 C++ 也这样做(在没有 virtual
inheritance/methods).
C 保证如果你有两个 struct
A
和 B
并且都以数据成员 char
开头,然后是数据成员int
(以及之后的任何内容),然后当您将它们放在 union
中时,您可以编写 B
成员并通过其 char
和 int
读取 A
成员,所以 C++ 也是:Standard Layout.
后者非常广泛,并且完全防止大多数struct
(或class
)的数据成员的任何重新排序。
请注意,标准确实允许一些重新排序:由于 C 没有访问控制的概念,C++ 指定未指定具有不同访问控制说明符的两个数据成员的相对顺序。
据我所知,没有编译器试图利用它;但理论上他们可以。
在 C++ 之外,Rust 等语言允许编译器对字段重新排序,而主 Rust 编译器 (rustc) 默认情况下这样做。只有历史决定和对向后兼容性的强烈愿望阻止了 C++ 这样做。
你还引用了 C++,所以我会给你一个实际的理由,为什么这不会发生。
给定 ,考虑:
class MyClass
{
string s;
anotherObject b;
MyClass() : s{"hello"}, b{s}
{}
};
现在 C++ 要求非静态数据成员按照声明的顺序进行初始化:
— Then, non-static data members are initialized in the order they were
declared in the class definition
根据 [base.class.init/13
]。所以编译器 不能 重新排序 class 定义中的字段,因为否则(例如)依赖于其他初始化的成员将无法工作。
编译器并没有严格要求不在内存中对它们重新排序(据我所知)——但是,特别是考虑到上面的例子,跟踪它会非常痛苦。而且我怀疑任何性能改进,与填充不同。
Dennis Ritchie 设计的语言不是根据行为定义结构的语义,而是根据内存布局。如果结构 S 在偏移量 X 处有一个类型为 T 的成员 M,那么 M.S 的行为被定义为获取 S 的地址,向其添加 X 个字节,将其解释为指向 T 的指针,并解释存储因此被识别为左值。编写结构成员会更改其关联存储的内容,而更改成员存储的内容会更改成员的值。代码可以自由使用多种方式来操纵与结构成员关联的存储,并且语义将根据对该存储的操作来定义。
代码可以操纵与结构关联的存储的有用方法之一是使用 memcpy() 将一个结构的任意部分复制到另一个结构的相应部分,或使用 memset() 清除任意部分的一个结构。由于结构成员是按顺序排列的,因此可以使用单个 memcpy() 或 memset() 调用复制或清除一系列成员。
标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储的要求,或者对存储的更改会影响成员值的要求,从而使结构布局的保证不如以前有用用里奇的语言。尽管如此,仍保留了使用 memcpy() 和 memset() 的能力,并且保留该能力需要保持结构元素的顺序。
假设我有这样一个结构:
struct MyStruct
{
uint8_t var0;
uint32_t var1;
uint8_t var2;
uint8_t var3;
uint8_t var4;
};
这可能会浪费一堆(不是一吨)space。这是因为 uint32_t
变量需要对齐。
实际上(在对齐结构以便它可以实际使用 uint32_t
变量之后)它可能看起来像这样:
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;
};
一个更有效的结构是:
struct MyStruct
{
uint8_t var0;
uint8_t var2;
uint8_t var3;
uint8_t var4;
uint32_t var1;
};
现在,问题是:
为什么(标准)禁止编译器对结构重新排序?
如果结构被重新排序,我看不出任何搬起石头砸自己脚的方法。
想象一下,这个结构布局实际上是一个接收到的内存序列 'over the wire',比如说一个以太网数据包。如果编译器重新调整事情以提高效率,那么您将不得不做大量工作以按要求的顺序提取字节,而不是仅仅使用具有正确顺序和位置的所有正确字节的结构。
I don't see any way you could shoot your self in the foot, if the struct was reordered.
真的吗?如果允许这样做,默认情况下 libraries/modules 之间的通信即使在同一进程中也会非常危险。
"In universe" 参数
我们必须能够知道我们的结构是按照我们要求的方式定义的。未指定填充已经够糟糕了!幸运的是,您可以在需要时控制它。
好的,理论上,可以创建一种新语言,类似地,成员可以重新排序除非给出某些属性。毕竟,我们不应该对对象进行内存级魔法,所以如果只使用 C++ 习惯用法,默认情况下你是安全的。
但这不是我们生活的实际现实。
"Out of universe" 参数
如果用您的话说,"the same reorder was used every time",您可以使事情变得安全。该语言必须明确说明成员的排序方式。这在标准中编写起来很复杂,理解起来也很复杂,实施起来也很复杂。
只需保证顺序与代码中的顺序一致,然后将这些决定留给程序员,就容易多了。请记住,这些规则起源于旧 C,旧 C 赋予 程序员 权力。
您已经在您的问题中展示了通过简单的代码更改使结构填充高效是多么容易。无需在语言级别增加任何复杂性即可为您执行此操作。
标准保证分配顺序只是因为结构可能表示特定的内存布局,例如数据协议或硬件寄存器集合。例如,程序员和编译器都不能随意重新安排 TPC/IP 协议中的字节顺序,或者微控制器的硬件寄存器。
如果不能保证顺序,structs
将仅仅是抽象的数据容器(类似于 C++ 向量),我们不能对其进行过多假设,只是它们以某种方式包含我们放入其中的数据他们。在进行任何形式的低级编程时,这会使它们变得更加无用。
The compiler should keep the order of its members in the case the structures are read by any other low-level code produced by another compiler or another language. Say you were creating an operating system, and you decide to write part of it in C, and part of it in assembly. You could define the following structure:
struct keyboard_input
{
uint8_t modifiers;
uint32_t scancode;
}
You pass this to an assembly routine, where you need to manually specify the memory layout of the structure. You would expect to be able to write the following code on a system with 4-byte alignment.
; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]
Now say the compiler would change the order of the members in the structure in an implementation defined way, this would mean that depending on the compiler you use and the flags you pass to it, you could either end up with the first byte of the scancode member in al, or with the modifiers member.
Of course the problem is not just reduced to low-level interfaces with assembly routines, but would also appear if libraries built with different compilers would call each other (e.g. building a program with mingw using the windows API).
Because of this, the language just forces you to think about the structure layout.
请记住,不仅自动重新排序元素以改进打包可能会损害特定的内存布局或二进制序列化,而且属性的顺序可能已被程序员仔细选择以有利于缓存局部性经常使用的成员与很少访问的成员。
Why is the compiler forbidden (by the standard) from reordering the struct?
基本原因是:为了兼容C.
请记住,C 最初是一种高级汇编语言。在 C 中,通过将字节重新解释为特定的 struct
.
这导致了依赖此 属性 的多个功能:
C 保证
struct
的地址和它的第一个数据成员的地址是相同的,所以 C++ 也这样做(在没有virtual
inheritance/methods).C 保证如果你有两个
struct
A
和B
并且都以数据成员char
开头,然后是数据成员int
(以及之后的任何内容),然后当您将它们放在union
中时,您可以编写B
成员并通过其char
和int
读取A
成员,所以 C++ 也是:Standard Layout.
后者非常广泛,并且完全防止大多数struct
(或class
)的数据成员的任何重新排序。
请注意,标准确实允许一些重新排序:由于 C 没有访问控制的概念,C++ 指定未指定具有不同访问控制说明符的两个数据成员的相对顺序。
据我所知,没有编译器试图利用它;但理论上他们可以。
在 C++ 之外,Rust 等语言允许编译器对字段重新排序,而主 Rust 编译器 (rustc) 默认情况下这样做。只有历史决定和对向后兼容性的强烈愿望阻止了 C++ 这样做。
你还引用了 C++,所以我会给你一个实际的理由,为什么这不会发生。
给定
class MyClass
{
string s;
anotherObject b;
MyClass() : s{"hello"}, b{s}
{}
};
现在 C++ 要求非静态数据成员按照声明的顺序进行初始化:
— Then, non-static data members are initialized in the order they were declared in the class definition
根据 [base.class.init/13
]。所以编译器 不能 重新排序 class 定义中的字段,因为否则(例如)依赖于其他初始化的成员将无法工作。
编译器并没有严格要求不在内存中对它们重新排序(据我所知)——但是,特别是考虑到上面的例子,跟踪它会非常痛苦。而且我怀疑任何性能改进,与填充不同。
Dennis Ritchie 设计的语言不是根据行为定义结构的语义,而是根据内存布局。如果结构 S 在偏移量 X 处有一个类型为 T 的成员 M,那么 M.S 的行为被定义为获取 S 的地址,向其添加 X 个字节,将其解释为指向 T 的指针,并解释存储因此被识别为左值。编写结构成员会更改其关联存储的内容,而更改成员存储的内容会更改成员的值。代码可以自由使用多种方式来操纵与结构成员关联的存储,并且语义将根据对该存储的操作来定义。
代码可以操纵与结构关联的存储的有用方法之一是使用 memcpy() 将一个结构的任意部分复制到另一个结构的相应部分,或使用 memset() 清除任意部分的一个结构。由于结构成员是按顺序排列的,因此可以使用单个 memcpy() 或 memset() 调用复制或清除一系列成员。
标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储的要求,或者对存储的更改会影响成员值的要求,从而使结构布局的保证不如以前有用用里奇的语言。尽管如此,仍保留了使用 memcpy() 和 memset() 的能力,并且保留该能力需要保持结构元素的顺序。