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

问题描述 投票:35回答:4

让我们考虑结构:

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中的填充中。什么是阻止它的规则,它背后的原因是什么?

c++ c struct memory-alignment
4个回答
20
投票

简短回答(对于问题的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 ++剩下的有趣案例是: 基础子对象,不受上述规则约束(参见C ++ 11 3.9 / 2),和 任何不是简单可复制类型的对象。

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

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


以前的答案一直供参考。

我相信这是因为S1是标准布局,因此由于某种原因S1S3子对象仍未受影响。我不确定这是否符合标准。

但是,如果我们将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


18
投票

我们来考虑一些代码:

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。


4
投票

这里有几个例子,为什么编译器不能将成员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(“类型代表/一般”)说:

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


0
投票

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

如果处理器认真对齐,则会引发异常/信号,否则会因为错位减慢数据访问而导致性能下降。

为了理解这一点,让我们从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字节对齐时,称存储器访问是对齐的。如果内存访问未对齐,则称其未对齐。

© www.soinside.com 2019 - 2024. All rights reserved.