【问题标题】:Packing unions/structure to avoid padding包装联合体/结构以避免填充
【发布时间】:2021-10-15 05:55:06
【问题描述】:

我的结构如下所示:

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)。

【问题讨论】:

  • 简短回答,否。该标准不允许控制对象的填充方式。
  • 8+12=20。你从哪里得到 16 个?
  • @VictorEijkhout 这是一个工会。它的大小将是最大成员的大小,加上可能的填充。
  • 欢迎来到C++的大伞。在许多平台上忽略对齐包装会使结构无法使用,而且它无法使用,这有什么意义呢?
  • 您可以将 A 更改为 std::byte A[sizeof(uint8_t*)];,然后将 memcpy 指针移入 A 并移出 A。

标签: c++ padding


【解决方案1】:

将 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 这样的单字节类型 - 我的意思是谢天谢地,否则就不可能创建内存池。如果它使事情更清晰/更安全,你甚至可以有一个明确的 char 数组缓冲区:

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

我相当肯定这会引起问题。

【讨论】:

  • 这是 UB,*reinterpret_cast&lt;int16_t*&gt; 导致严格的别名冲突。
  • @rustyx 是否存在位置 12 和 14 至少不是 2 对齐的情况?我认为这类似于拥有一个 char* 的内存池并通过对 char 数组的对齐施加限制来构建您的 T 对象。
  • @NikosAthanasiou 对齐和严格别名是单独的问题,将 char 数组读取为更大的整数也是 UB
  • @M.M 如果缓冲区是char*(即像uint8_t 这样的单字节类型),则没有严格的别名冲突——这是规则中的例外。我在演示中启用了严格的混叠警告,但它没有给出任何积极的信息。如果您尝试将类型大小加倍(使用 uint16_t 缓冲区并制作 C&D uint32_t),您将收到别名冲突警告。案例过于简单,不会有误报警告。
  • @rustyx 我在答案中尽我所能解决了严格的混叠假设。如果现在看起来更好或者有反例,请告诉我
【解决方案2】:

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

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

另外添加一个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;
  }
};

【讨论】:

    【解决方案3】:

    据我了解,以下代码是最安全的。

    指定类型的数据在初始公共序列中。因此,您可以通过任何方式访问它(使用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 字节可能没什么大不了的。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-04-05
      • 1970-01-01
      • 2016-12-05
      • 1970-01-01
      • 2016-06-27
      • 2010-10-26
      • 2010-11-02
      • 1970-01-01
      相关资源
      最近更新 更多