【问题标题】:When extending a padded struct, why can't extra fields be placed in the tail padding?扩展填充结构时,为什么不能在尾部填充中放置额外的字段?
【发布时间】:2014-07-29 09:46:18
【问题描述】:

让我们考虑结构:

struct S1 {
    int a;
    char b;
};

struct S2 {
    struct S1 s;       /* struct needed to make this compile as C without typedef */
    char c;
};

// For the C++ fans
struct S3 : S1 {
    char c;
};

S1 的大小为 8,由于对齐,这是预期的。但是 S2 和 S3 的大小是 12。这意味着编译器将它们构造为:

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11|
|       a       | b |  padding  | c |  padding  |

编译器可以在不破坏对齐约束的情况下将 c 放在 6 7 8 的填充中。阻止它的规则是什么,背后的原因是什么?

【问题讨论】:

  • 这当然并不意味着编译器一定是这样布局的。
  • 在制作这些结构的数组时,仍然需要正确对齐。最后需要额外的填充。
  • @HansPassant:这并不能解释为什么 S3 不能放入 8 个字节中......
  • 因此您希望编译器根据 S1S2 的一部分还是单独出现来以不同方式处理它?
  • 这是一个语言设计者需要考虑的有趣问题。当我们为 IBM iSeries 实现 Java 时,我们发现如果我们“打包”结构(同时仍保持硬件首选的对齐方式),我们节省了足够的存储空间,从而提高了大约 10% 的性能,这是由于缓存使用的改进和减少的垃圾收集负载。 (当然,Java 标准对对象中变量的顺序或布局没有任何限制,因此我们可以摆脱这一点,因为 C++ 实现者就没有那么幸运了。)

标签: c++ c struct memory-alignment


【解决方案1】:

简答(针对问题的 C++ 部分)出于历史原因,Itanium ABI for C++ 禁止使用 POD 类型的基本子对象的尾部填充.请注意,C++11 没有这样的禁令。允许通过底层表示复制普通可复制类型的相关规则 3.9/2 明确排除了基本子对象。


长答案:我将尝试同时处理 C++11 和 C。

  1. S1 的布局必须包含填充,因为S1::a 必须与int 对齐,并且数组S1[N]S1 类型的连续分配对象组成,每个a 成员必须保持一致。
  2. 在 C++ 中,不是基本子对象的普通可复制类型 T 的对象可以被视为 sizeof(T) 字节的数组(即,您可以将对象指针强制转换为 unsigned char * 并将结果视为指向unsigned char[sizeof(T)] 的第一个元素的指针,这个数组的值决定了对象)。由于 C 中的所有对象都属于这种类型,这就解释了 C 和 C++ 的 S2
  3. C++ 剩下的有趣案例是:
    1. 不受上述规则约束的基础子对象(参见 C++11 3.9/2),以及
    2. 任何非普通可复制类型的对象。

对于 3.1,确实存在常见的流行“基本布局优化”,其中编译器将类的数据成员“压缩”到基本子对象中。这在基类为空时最为显着(减小了 ∞% 的大小!),但应用更广泛。但是,当相应的基本类型为 POD(并且 POD 表示可简单复制和标准布局)时,我在上面链接的 Itanium ABI for C++ 以及许多编译器实现的 Itanium ABI 禁止这种尾部填充压缩。

对于 3.2,Itanium ABI 的相同部分适用,尽管我目前不认为 C++11 标准实际上要求任意、非平凡可复制的 member 对象必须具有与同一类型的完整对象大小相同。


保留以前的答案以供参考。

我相信这是因为S1 是标准布局,因此出于某种原因S3S1-子对象保持不变。我不确定这是否是标准规定的。

但是,如果我们将S1 转换为非标准布局,我们会观察到布局优化:

struct EB { };

struct S1 : EB {   // not standard-layout
    EB eb;
    int a;
    char b;
};

struct S3 : S1 {
    char c;
};

现在sizeof(S1) == sizeof(S3) == 12 在我的平台上。 Live demo.

这是simpler example

struct S1 {
private:
    int a;
public:
    char b;
};

struct S3 : S1 {
    char c;
};

混合访问使S1 非标准布局。 (现在sizeof(S1) == sizeof(S3) == 8。)

更新:定义因素似乎是琐碎以及标准布局,即类必须是 POD。以下非 POD 标准布局类可进行基本布局优化:

struct S1 {
    ~S1(){}
    int a;
    char b;
};

struct S3 : S1 {
    char c;
};

再次sizeof(S1) == sizeof(S3) == 8Demo

【讨论】:

  • 有人有C++03标准吗?编译器可能正试图保持与 C++03 的某种兼容性,后者具有不同的类和联合类型分类以及不同的memcpy 规则。
  • 仍然没有真正解释为什么,但至少表明我没有疯,这样的布局是可能的并且是有意义的,甚至解释了在什么条件下会使用或不使用这种布局。公认。仍然愿意知道为什么这些规则是这样的。
  • 这根本不能回答问题的 C 部分,这纯粹是因为类型的大小不能根据它出现的上下文而改变,而且事实上结构可以在数组中使用。
  • 如果您将S1::aS2::b 设为私有并删除EB,则S1 仍然是POD,但是,尾部填充在S3 中重复使用。
【解决方案2】:

让我们考虑一些代码:

struct S1 {
    int a;
    char b;
};

struct S2 {
    S1 s;
    char c;
};

让我们考虑一下如果sizeof(S1) == 8sizeof(S2) == 8 会发生什么。

struct S2 s2;
struct S1 *s1 = &(s2.s);
memset(s1, 0, sizeof(*s1));

您现在已经覆盖了S2::c


出于数组对齐的原因,S2 的大小也不能为 9、10 或 11。因此下一个有效大小是 12。

【讨论】:

  • 只有当你能以某种方式证明应该能够使用memset 来访问对象的表示时,这才会变成一个论点。我想应该适用某种trivially_copyablestandard_layout 规则。
  • @KerrekSB:我从 C 的角度出发,所有东西都有标准布局(至少我相信这是真的)。​​
  • 好吧,C 没有继承,所以问题中唯一有趣的部分(基本子对象布局)不适用。如果 C++ 有某种兼容性规则,这在某种程度上是有道理的,但我不知道它是什么。
  • @KerrekSB 此答案中的 S1 和 S2 示例不依赖于继承。
  • C 传统让你做类似 memset 之类的事情,并且可以写入写入以避免弄脏 CPU 缓存(或需要读取整个缓存行),只需使用写组合。因此,从低级系统编程的角度来看,这是一种有价值的技术。 C++ 的创建者在其 C++ 语言的核心 C 子集中重视 C 兼容性。
【解决方案3】:

以下是编译器无法将成员 c 放置在 struct S1 成员 s 的尾随填充中的几个示例。假设编译器确实将struct S2.c 放在struct S1.s. 成员的填充中:

struct S1 {
    int a;
    char b;
};

struct S2 {
    struct S1 s;       /* struct needed to make this compile as C without typedef */
    char c;
};

// ...

struct S1 foo = { 10, 'a' };
struct S2 bar = {{ 20, 'b'}, 'c' };

bar.s = foo;    // this will likely corrupt bar.c

memcpy(&bar.s, &foo, sizeof(bar.s));    // this will certainly corrupt bar.c

bar.s.b = 'z';  // this is permited to corrupt bar by C99 6.2.6.1/6

C99/C11 6.2.6.1/6(“类型/一般表示”)说:

当一个值存储在结构或联合类型的对象中时, 包括在成员对象中,对象表示的字节 对应于任何填充字节的值采用未指定的值。

【讨论】:

  • 这并没有真正解释原因,但肯定会增加讨论。感谢您对标准的参考。
  • 我认为这是对原因的充分解释 - 本质上,如果编译器将结构的另一个成员放在嵌入式结构的尾随填充中,则填充不能包含在嵌入的大小中结构。如果是这种情况,那么结构就没有填充。
  • @MichaelBurr:允许尾部填充需要 C 包含单独的“sizeof”和“strideof”运算符(因此“sizeof (struct s1)”将为 5,但“ strideof (struct S1)" 将是 8)。一个相关的有用概念是类型具有单独的“首选”和“必需”对齐。工会的“首选”路线将是其任何成员的首选路线中最粗略的;所需的对齐将是其任何成员所需对齐中最好的。一个正确对齐的指向联合成员的指针将满足联合类型所需的对齐方式,...
  • ...前提是它仅用于访问与其对齐的成员;相比之下,满足联合“首选”对齐方式的指针可用于访问所有成员。
【解决方案4】:

结构中的额外填充背后的原因是什么?

如果处理器对 alignment 很认真,它会引发异常/信号,否则会因为未对齐减慢数据访问而导致性能损失。

要理解这一点,让我们从data structure alignment开始:

数据结构对齐是数据在计算机内存中排列和访问的方式。它由两个独立但相关的问题组成:数据对齐数据结构填充。当现代计算机读取或写入内存地址时,它将以字大小的块(例如 32 位系统上的 4 字节块)或更大的形式执行此操作。 数据对齐意味着将数据放置在等于字大小的某个倍数的内存偏移处,由于 CPU 处理内存的方式,这会提高系统的性能。为了对齐数据,可能需要在最后一个数据结构的结尾和下一个数据结构的开头之间插入一些无意义的字节,这就是数据结构填充。

例如,当计算机的字长为 4 字节时(一个字节在大多数机器上表示 8 位,但在某些系统上可能不同),要读取的数据应位于内存偏移量为 4 的某个倍数. 如果不是这种情况,例如数据从14th 字节而不是16th 字节开始,然后计算机必须读取两个 4 字节块并在读取请求的数据之前进行一些计算,否则可能会产生对齐错误强>。即使前一个数据结构在第 13 个字节结束,下一个数据结构也应该从第 16 个字节开始。在两个数据结构之间插入两个填充字节,以将下一个数据结构与第 16 个字节对齐。


扩展填充结构时,为什么不能在尾部填充中放置额外的字段?

编译器可以在不破坏对齐约束的情况下将 c 放在 6 7 8 的填充中。阻止它的规则是什么,背后的原因是什么?

编译器可以将它放在那里,但是对c 的内存访问会被误用1,并且会产生性能损失,如上所述。要处理数组:

struct __attribute__((__packed__)) mypackedstruct{
    char a;
    int b;
    char c;
};  

此结构在 32 位系统上的编译大小为 6 字节。
在允许它的架构(如 x86 和 amd64)上,未对齐的内存访问速度较慢,并且在 SPARC 等严格对齐的架构上被明确禁止。


1 当被访问的数据为n 字节长(其中n 是2 的幂)且数据地址为n 时,内存访问称为对齐-字节对齐。当内存访问未对齐时,称为未对齐。

【讨论】:

  • 我很清楚数据对齐。这不是问题所在。我在问题和 cmets 中特别提到了这一点。
  • 我把这个问题读了三次,发现你在数据对齐方面遗漏了一些东西。阅读答案的第二部分。
猜你喜欢
  • 2020-04-30
  • 1970-01-01
  • 2011-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-26
  • 2023-03-03
  • 1970-01-01
相关资源
最近更新 更多