【问题标题】:Why does GCC pad this bit-field?为什么 GCC 填充这个位域?
【发布时间】:2023-04-02 03:58:01
【问题描述】:

程序在 C 中使用 std=c99,这是在 64 位机器上。

struct epochs {
    volatile unsigned int epoch    : 1;
    volatile unsigned int pulse    : 1;
    volatile unsigned int active0  : 7;
    volatile unsigned int active1  : 7;
    volatile unsigned int counter0 : 24; 
    volatile unsigned int counter1 : 24; 
};

当我检查 sizeof(epochs) 时,它给了我 12。

我可以告诉 gcc 不要通过添加 __attribute((packed)); 来填充它。所以我可以解决它。但是我真的很想知道为什么要添加 4 个字节来填充这个 64 位结构?

这里主要是这个结构需要 64 位,因为它在 64 位原子交换操作中一次全部更新,这当然不适用于 12 字节值。

【问题讨论】:

  • 通常允许对位域进行填充以提高性能,我最好的猜测是编译器在您的数据成员之间添加了填充,以确保它们在字节边界上对齐。添加打包属性确实会告诉编译器不要添加填充以提高性能。
  • 如果您想要可移植/可重现的行为,请不要使用位域。
  • struct epochs { unsigned int counter0 : 24, pulse : 1, active0 : 7, counter1 : 24, epoch : 1, active1 : 7; }; 在我的 linux、solaris 和 aix 上是 8 个字节。 (顺便说一句,为什么都是volatile?)
  • @Exodist volatile 不提供这些语义(但原子操作提供)
  • @Exodist 有 atomic loads in gcc,它们在很大程度上遵循 C11/C++11 模型。

标签: c struct padding atomic compare-and-swap


【解决方案1】:
volatile unsigned int epoch    : 1;
volatile unsigned int pulse    : 1;
volatile unsigned int active0  : 7;
volatile unsigned int active1  : 7;

^ 32 位(4 字节)

volatile unsigned int counter0 : 24; 

^ 32 位(4 字节)

volatile unsigned int counter1 : 24; 

^ 32 位(4 字节)

所以还有 4 个字节。

C 说:

(C99, 6.7.2.1p10) “如果有足够的空间,结构中紧跟在另一个位域之后的位域应该被打包到同一单元的相邻位中”

没有足够的空间将更多的 24 位 (counter0) 放入已经拥有 16 位 (epoch, @987654327) 的 32 位单元(可能是您系统中 unsigned int 的大小) @、active0active1)。

您可以使用uin64_t 而不是使用unsigned int 将您的位域打包成一个 64 位单元,但无论您的系统是否支持它都是由实现定义的。

(C99, 6.7.2.1p4) "位域的类型应该是 _Bool、signed int、unsigned int、或其他一些实现定义的类型的合格或非合格版本。”

【讨论】:

  • 我认为如果它注意到前四个字段使用 16 位,然后明确表示将接下来的 24 位紧跟在它们之后将跨越 32 位边界,这会更清楚。并且 GCC 可能会避免这种情况,因为访问跨越边界的位字段将需要两个加载指令(取决于目标处理器)和额外的位操作指令来组合加载的值。跨边界存储会更糟。因此,位域分配首选 32 位单元。
  • 这个答案正是我所需要的。根据 32 位对齐排列我的字段也意味着我可以避免填充:counter0、epoch、active0、counter1、pulse、active1。这使它保持在 64 位。
  • @EricPostpischil 如果我使用#pragma pack(),我得到的结构大小是 8 个字节(不是 12 个字节)。这里的内存分配是如何发生的?
  • @EswaranPandi:此答案示例中的六个字段需要 1+1+7+7+24+24 = 64 位,即 8 个 8 位字节。如果,当您请求打包时,您的编译器将它们放入 8 个字节中,那么很明显它们被打包成没有填充的字节。但是,编译器可以按字节或例如 32 位字来组织字段,并且可以将字段从单元的低位到高位进行排序,反之亦然。
【解决方案2】:

虽然一些较旧的编译器曾经将int foo:3; 视为例如的同义词。 long foo:3,或short foo:3,并以任何方便的方式简单地放置foo,当前的C标准规定每个位字段必须完全适合适当大小的存储单元。我不知道该规范的基本原理是什么,因为指定位字段的方式仍然过于模糊,无法在可移植代码中使用它们,但有时无法以最佳方式打包内容。例如,在结构中有效存储 24 位值的唯一方法是让一台机器在支持 32 位整数的机器上,或者有一个 8 位数据可以放在相邻的位置。 24 位值(之前或之后),以便“填写”一个 32 位字。

幸运的是,在您的特定情况下,可以通过重新排列字段来避免效率低下。如果您的编译器支持使用这种类型的位域,也可以通过将每个字段的声明类型更改为unsigned long long 来避免效率低下[在这种情况下,如果位域没有跨越 32 位边界,则允许它们跨越 32 位边界。 t 跨越 64 位边界]。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-10-03
    • 1970-01-01
    • 1970-01-01
    • 2011-12-16
    • 2020-02-14
    • 2014-04-11
    相关资源
    最近更新 更多