Packing unions/structure to avoid padding

Viewed 297

I have a structure that looks like this:

struct vdata {
  static_assert(sizeof(uint8_t *) == 8L, "size of pointer must be 8");
  union union_data {
    uint8_t * A; // 8 bytes
    uint8_t B[12]; // 12 bytes
  } u;
  int16_t C; // 2 bytes
  int16_t D; // 2 bytes
};

I would like to make this 16 bytes, but GCC is telling me it is 24, as the union is padding to 16 bytes.

I would like to put vdata into a large std::vector. From my understanding, there should be no issue with alignment if this were 16 bytes, since the pointer would always be 8 byte aligned.

I understand that I can force this to be packed using __attribute__((__packed__)) in GCC. But I would like to know if there is a portable and standard compliant way to get this to be 16 bytes?


Edit: Ideas

Idea 1: split up the B array.

struct vdata {
  union union_data {
    uint8_t * A; // 8 bytes
    uint8_t B[8]; // 8 bytes
  } u;
  uint8_t B2[4]; // 4 bytes
  int16_t C; // 2 bytes
  int16_t D; // 2 bytes
};

Could B2 elements be reliably accessed from a pointer of B? Is that defined behavior?

Idea 2: store pointer as byte array and memcpy as necessary (@Eljay)

struct vdata {
  union union_data {
    std::byte A[sizeof(uint8_t*)]; // 8 bytes
    uint8_t B[12]; // 12 bytes
  } u;
  int16_t C; // 2 bytes
  int16_t D; // 2 bytes
};

Would there be a performance penalty for accessing the pointer, or would it be optimized out? (Assuming GCC x86).

3 Answers

You could change A to std::byte A[sizeof(uint8_t*)]; and then std::memcpy the pointer into A and out of A.

Worth commenting as to what is going on, and that these extra hoops are to avoid padding bytes.

Also adding a set_A setter and get_A getter may be very helpful.

struct vdata {
  union union_data {
    std::byte A[sizeof(uint8_t*)]; // 8 bytes
    uint8_t B[12]; // 12 bytes
  } u;
  int16_t C; // 2 bytes
  int16_t D; // 2 bytes

  void set_A(uint8_t* p) {
    std::memcpy(u.A, &p, sizeof p); 
  }
  uint8_t* get_A() {
    uint8_t* result;
    std::memcpy(&result, u.A, sizeof result);
    return result;
  }
};

Store C+D in the union's array, and provide method access to them:

struct vdata {
  static_assert(sizeof(uint8_t *) == 8L, "size of pointer must be 8");
 
   union union_data {
    uint8_t * A;   // 8 bytes
    uint8_t B[16]; // 12 + 2*2 bytes
  } u;
  
  int16_t& C() { 
    return *reinterpret_cast<int16_t*>(static_cast<void*>(&u.B[12])); 
  }
  int16_t& D() { 
    return *reinterpret_cast<int16_t*>(static_cast<void*>(&u.B[14])); 
  }
};

Demo (with zero warnings for strict aliasing violations and run-time address sanitization enabled)

Keep in mind that there's no strict aliasing violation when the buffer is char* i.e. single byte type like uint8_t - I mean thankfully because otherwise it would be impossible to create memory pools. If it makes things clearer/safer you can even have an explicit char array buffer:

struct vdata {
   union union_data {
    uint8_t * A;   // 8 bytes
    uint8_t B[12]; // 12 bytes
    char buf[16];  // 16 bytes - could be std::byte buf[16]
  } u;
  
  int16_t& C() { return *(int16_t*)(&u.buf[12]); }
  int16_t& D() { return *(int16_t*)(&u.buf[14]); }
};

Regarding alignment The array is 8-aligned due to the address of the union, so positions 12&14 are guaranteed to be 2-aligned which is the requirement for int16_t (even though the string u.B appears in the code).


Alternatively you can force align the structure. The C++ alignas specifier would not be valid here because you want to lower the alignment of your structure, put a pragma directive is possible to give you again 16 bytes:

#pragma pack(4)
struct vdata {
  static_assert(sizeof(uint8_t *) == 8L, "size of pointer must be 8");
  union union_data {
    uint8_t * A; // 8 bytes
    uint8_t B[12]; // 12 bytes
  } u;
  int16_t C; // 2 bytes
  int16_t D; // 2 bytes
};

Demo

I'm fairly certain that this one will cause problems.

As far as I understand, the following code would be the most safe one.

The data that specify the type is in the Initial common sequence. Thus you can access it either way (by using cda.C or cdb.C) so it is perfect for determining the type.

Then putting everything in a struct for both cases allows to ensure that each struct layout is independant (thus B can start before next 8 bytes alignment).

#include <cstdint>
#include <iostream>

struct CDA
{
    int16_t C;      // 2 bytes
    int16_t D;      // 2 bytes
    uint8_t* A;     // 8 bytes
};

struct CDB
{
    int16_t C;      // 2 bytes
    int16_t D;      // 2 bytes
    uint8_t B[12];  // 12 bytes
};

struct vdata {
    union union_data {
        CDA cda;
        CDB cdb;
    } u;

};

static_assert(sizeof(uint8_t*) == 8);
static_assert(sizeof(CDA) == 16);
static_assert(sizeof(CDB) == 16);
static_assert(offsetof(vdata::union_data, cda) == offsetof(vdata::union_data, cdb));
static_assert(offsetof(CDA, C) == offsetof(CDB, C));
static_assert(offsetof(CDA, C) == 0);
static_assert(sizeof(vdata) == 16);

int main()
{
    std::cout << "sizeof(CDA) : " << sizeof(CDA) << std::endl;
    std::cout << "sizeof(CDB) : " << sizeof(CDB) << std::endl;
    std::cout << "sizeof(vdata) : " << sizeof(vdata) << std::endl;
}

Usefull source of information:

How to decide?

  • If the size optimization is not that important, I would recommend to use std::variant.
  • If the size is important but the order is not, then the current solution might be the best choice.
  • If portability is not so important, then pragma pack solution might be appropriate (remember to reset alignment after the struct definition).
  • Otherwise, if you really need layout control, then either use:
    • std::byte array and memcpy (access data with functions)
    • placement new and std::launder.

In all cases, be sure to have appropriate assertion that verify assumptions you make. I have put many in my sample code but you can adjust depending on your need.

Also, unless you have millions of vdata items or you are on an embedded device, then using 24 bytes instead of 16 might not be a big deal.

You might also use conditionnal define to optimize only for your current compiler. This could be useful to ensure that you have working code (though maybe less optimal) for every target or it can allows to depend on behavior that is undefined from the standard but might be defined on your compiler.

Related