When extending a padded struct, why can't extr

2019-01-18 08:18发布

问题:

Let's consider the structs :

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;
};

The size of S1 is 8, which is expected due to alignment. But the size of S2 and S3 is 12. Which means the compiler structure them as :

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

The compiler could place c in the padding in 6 7 8 without breaking alignment constraints. What is the rule that prevent it, and what is the reason behind it ?

回答1:

Short answer (for the C++ part of the question): The Itanium ABI for C++ prohibits, for historical reasons, using the tail padding of a base subobject of POD type. Note that C++11 does not have such a prohibition. The relevant rule 3.9/2 that allows trivially-copyable types to be copied via their underlying representation explicitly excludes base subobjects.


Long answer: I will try and treat C++11 and C at once.

  1. The layout of S1 must include padding, since S1::a must be aligned for int, and an array S1[N] consists of contiguously allocated objects of type S1, each of whose a member must be so aligned.
  2. In C++, objects of a trivially-copyable type T that are not base subobjects can be treated as arrays of sizeof(T) bytes (i.e. you can cast an object pointer to an unsigned char * and treat the result as a pointer to the first element of a unsigned char[sizeof(T)], and the value of this array determines the object). Since all objects in C are of this kind, this explains S2 for C and C++.
  3. The interesting cases remaining for C++ are:
    1. base subobjects, which are not subject to the above rule (cf. C++11 3.9/2), and
    2. any object that is not of trivially-copyable type.

For 3.1, there are indeed common, popular "base layout optimizations" in which compilers "compress" the data members of a class into the base subobjects. This is most striking when the base class is empty (∞% size reduction!), but applies more generally. However, the Itanium ABI for C++ which I linked above and which many compilers implement forbids such tail padding compression when the respective base type is POD (and POD means trivially-copyable and standard-layout).

For 3.2 the same part of the Itanium ABI applies, though I don't currently believe that the C++11 standard actually mandates that arbitrary, non-trivially-copyable member objects must have the same size as a complete object of the same type.


Previous answer kept for reference.

I believe this is because S1 is standard-layout, and so for some reason the S1-subobject of S3 remains untouched. I'm not sure if that's mandated by the standard.

However, if we turn S1 into non-standard layout, we observe a layout optimization:

struct EB { };

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

struct S3 : S1 {
    char c;
};

Now sizeof(S1) == sizeof(S3) == 12 on my platform. Live demo.

And here is a simpler example:

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

struct S3 : S1 {
    char c;
};

The mixed access makes S1 non-standard-layout. (Now sizeof(S1) == sizeof(S3) == 8.)

Update: The defining factor seems to be triviality as well as standard-layoutness, i.e. the class must be POD. The following non-POD standard-layout class is base-layout optimizable:

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

struct S3 : S1 {
    char c;
};

Again sizeof(S1) == sizeof(S3) == 8. Demo



回答2:

Let's consider some code:

struct S1 {
    int a;
    char b;
};

struct S2 {
    S1 s;
    char c;
};

Let's consider what would happen if sizeof(S1) == 8 and sizeof(S2) == 8.

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

You've now overwritten S2::c.


For array alignment reasons, S2 also cannot have a size of 9, 10, or 11. So the next valid size is 12.



回答3:

Here are a couple of examples why a compiler can't place member c in the trailing padding of the struct S1 member s. Assume for the following that the compiler did place struct S2.c in the padding of the struct S1.s. member:

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 ("Representation of types/general") says:

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.



回答4:

What is the reason behind the extra padding in structs?

If the processor is serious about alignment it raises an exception/signal , otherwise a performance penalty will be there as misalignment slowdown data access.

To understand this let's start with data structure alignment:

Data structure alignment is the way data is arranged and accessed in computer memory. It consists of two separate but related issues: data alignment and data structure padding. When a modern computer reads from or writes to a memory address, it will do this in word sized chunks (e.g. 4 byte chunks on a 32-bit system) or larger. Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system's performance due to the way the CPU handles memory. To align the data, it may be necessary to insert some meaningless bytes between the end of the last data structure and the start of the next, which is data structure padding.

For example, when the computer's word size is 4 bytes (a byte means 8 bits on most machines, but could be different on some systems), the data to be read should be at a memory offset which is some multiple of 4. When this is not the case, e.g. the data starts at the 14th byte instead of the 16th byte, then the computer has to read two 4 byte chunks and do some calculation before the requested data has been read, or it may generate an alignment fault. Even though the previous data structure ends at the 13th byte, the next data structure should start at the 16th byte. Two padding bytes are inserted between the two data structures to align the next data structure to the 16th byte.


When extending a padded struct, why can't extra fields be placed in the tail padding?

The compiler could place c in the padding in 6 7 8 without breaking alignment constraints. What is the rule that prevent it, and what is the reason behind it ?

Compiler could place it there but then the memory access to c will misalinged1 and there will be a performance penalty as explained above.To aling an array:

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

This structure would have a compiled size of 6 bytes on a 32-bit system.
Unaligned memory access is slower on architectures that allow it (like x86 and amd64), and is explicitly prohibited on strict alignment architectures like SPARC.


1 A memory access is said to be aligned when the datum being accessed is n bytes long (where n is a power of 2) and the datum address is n-byte aligned. When a memory access is not aligned, it is said to be misaligned.