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

时间:2014-06-08 20:10:27

标签: c++ c struct memory-alignment

让我们考虑结构:

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

4 个答案:

答案 0 :(得分: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 ++剩下的有趣案例是:
    1. 基础子对象,不受上述规则约束(参见C ++ 11 3.9 / 2),
    2. 任何非平凡可复制类型的对象。
  4. 对于3.1,确实存在常见的,流行的“基本布局优化”,其中编译器将类的数据成员“压缩”到基础子对象中。当基类为空(∞%大小减小!)时,这是最引人注目的,但更普遍适用。但是,我在上面链接的Itanium ABI for C ++,当各自的基类型为POD时,许多编译器实现的禁止这种尾部填充压缩(而POD意味着可以简单地复制和标准布局)。

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


    以前的答案一直供参考。

    我认为这是因为S1是标准布局,因此S1 - S3的子对象保持不变。我不确定这是否符合标准。

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

答案 1 :(得分: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。

答案 2 :(得分: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("类型代表/一般")说:

  

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

答案 3 :(得分:0)

  

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

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

要理解这一点,请从data structure alignment开始:

  

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

     

例如,当计算机的字大小为4个字节(一个字节在大多数机器上意味着8位,但在某些系统上可能不同)时,要读取的数据应该是内存偏移量,即4的一些倍数。当不是这种情况时,例如数据从14字节而不是16字节开始,然后计算机必须读取两个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的幂)并且数据地址为{时,称对齐内存访问{1}} - 字节对齐。如果内存访问未对齐,则称其未对齐。