打包 unions/structure 以避免填充

Packing unions/structure to avoid padding

我的结构如下所示:

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

我想把它设为 16 字节,但 GCC 告诉我它是 24,因为联合正在填充到 16 字节。

我想把vdata放到一个大的std::vector。根据我的理解,如果这是 16 个字节,则对齐应该没有问题,因为指针始终是 8 个字节对齐的。

我知道我可以在 GCC 中使用 __attribute__((__packed__)) 强制打包它。但我想知道是否有一种可移植且符合标准的方法可以将其设置为 16 字节?


编辑:想法

思路一:拆分B数组

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

能否从 B 的指针可靠地访问 B2 元素?这是定义的行为吗?

想法 2:将指针存储为字节数组并根据需要进行 memcpy (@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
};

访问指针会导致性能下降,还是会被优化掉? (假设 GCC x86)。

将 C+D 存储在联合数组中,并提供对它们的方法访问:

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(针对严格的别名违规的警告为零,并启用了 运行-时间地址清理)

请记住 there's no strict aliasing violation 当缓冲区为 char* 时,即像 uint8_t 这样的单字节类型 - 我的意思是谢天谢地,否则将无法创建内存池。如果它使东西 clearer/safer 你甚至可以有一个明确的字符数组缓冲区:

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

关于对齐 由于联合的地址,数组是 8 对齐的,因此位置 12 和 14 保证是 2 对齐的,这是 int16_t 的要求(即使字符串 u.B出现在代码中)。


或者您可以强制对齐结构。 C++ alignas 说明符在这里无效,因为您想降低结构的对齐方式,放置 pragma directive 可以再次给您 16 个字节:

#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

我相当确定这会导致问题。

您可以将 A 更改为 std::byte A[sizeof(uint8_t*)];,然后将指针 std::memcpy 更改为 A 并从 A 中更改。

值得评论发生了什么,这些额外的环是为了避免填充字节。

另外添加 set_A setter 和 get_A getter 可能会很有帮助。

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

据我所知,下面的代码是最安全的。

指定类型的数据在初始公共序列中。因此,您可以通过任何一种方式访问​​它(通过使用 cda.Ccdb.C),因此它非常适合确定类型。

然后将两种情况下的所有内容都放在一个结构中可以确保每个结构布局都是独立的(因此 B 可以在下一个 8 字节对齐之前开始)。

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

有用的信息来源:

如何决定?

  • 如果尺寸优化不是那么重要,我会推荐使用std::variant
  • 如果大小很重要但顺序不重要,那么当前的解决方案可能是最佳选择。
  • 如果可移植性不是那么重要,那么 pragma pack 解决方案可能是合适的(记得在结构定义后重置对齐方式)。
  • 否则,如果你真的需要布局控制,那么要么使用:
    • std::byte 数组和 memcpy(使用函数访问数据)
    • 展示位置 newstd::launder.

在所有情况下,请确保有适当的断言来验证您所做的假设。我在示例代码中放了很多,但您可以根据需要进行调整。

此外,除非您有数百万个 vdata 项或者您使用的是嵌入式设备,否则使用 24 个字节而不是 16 个可能不是什么大问题。

您也可以使用 conditionnal define 仅针对您当前的编译器进行优化。这可能有助于确保每个目标都有工作代码(尽管可能不太理想),或者它可以允许依赖于标准中未定义但可能在编译器上定义的行为。