结构的大小是不是需要是该结构对齐的精确倍数?

Posted

技术标签:

【中文标题】结构的大小是不是需要是该结构对齐的精确倍数?【英文标题】:Is the size of a struct required to be an exact multiple of the alignment of that struct?结构的大小是否需要是该结构对齐的精确倍数? 【发布时间】:2011-06-05 23:37:35 【问题描述】:

我再次质疑一个长期存在的信念。

直到今天,我都认为下面这个结构体的对齐通常是4,大小通常是5...

struct example

  int   m_Assume_32_Bits;
  char  m_Assume_8_Bit_Bytes;
;

由于这个假设,我的数据结构代码使用 offsetof 来确定数组中两个相邻项之间的字节距离。今天,我发现一些旧代码在不应该使用 sizeof 的地方使用,不明白为什么我没有其中的错误,编写了一个单元测试 - 测试通过让我感到惊讶。

一些调查表明,我用于测试的类型的大小(类似于上面的结构)是对齐的精确倍数 - 即 8 个字节。它在最后一个成员之后有填充。这是一个示例,说明为什么我从未预料到会发生这种情况...

struct example2

  example m_Example;
  char    m_Why_Cant_This_Be_At_Offset_6_Bytes;
;

一些谷歌搜索显示的示例清楚地表明允许在最终成员之后进行此填充 - 例如 http://en.wikipedia.org/wiki/Data_structure_alignment#Data_structure_padding(“或在结构的末尾”位)。

这有点尴尬,因为我最近发布了这条评论 - Use of struct padding(我对该答案的第一条评论)。

我似乎无法确定这种填充到对齐的精确倍数是否由 C++ 标准保证,或者它是否只是允许的并且某些(但可能不是全部)编译器会这样做。

那么 - 根据 C++ 标准,结构的大小是否必须是该结构对齐的精确倍数?

如果 C 标准做出不同的保证,我也对此感兴趣,但重点是 C++。

【问题讨论】:

是的,它是必需的。如果不是这样,您将无法对数组进行动态分配(使用 malloc)。 @Ben - +1 您在下面的中肯回答,但是,根据您的评论,如果您可以假设数组中的项目与结构中的相同类型字段填充相同(没有其他类型的成员),您可以将数组的每项填充大小计算为具有该类型的两个成员(仅此而已)的结构中第二个成员的偏移量。这就是我一直在做的错误。 【参考方案1】:

alignment大小的一种定义:

结构的 alignment 大小是当您拥有该结构的数组时从一个元素到下一个元素的偏移量。

就其本质而言,如果您有一个包含两个元素的结构数组,那么两者都需要有对齐的成员,这意味着是的,大小必须是对齐的倍数。 (我不确定是否有任何标准明确强制执行此操作,但由于结构的大小和对齐方式不取决于结构是单独的还是在数组中,所以相同的规则适用于两者,所以它不能真的任何其他方式。)

【讨论】:

为什么编译器不能在数组中的结构实例之间添加填充?是否有一些直接(不是间接暗示)保证,可能基于使用 cast-to-char* 指针算法的旧习惯用法,每个结构实例将从数组中的前一个开始 sizeof(T) 字符? 因为如果它为数组而不是普通实例做额外的填充,那么你不能用指针遍历它。 (不确定这是否回答了您的问题?) @Lambert - 为什么不呢?使用指针进行迭代肯定被指定为与数组兼容,并且这并不意味着(尽管可能)指针增量距离(以字节为单位)与数组中项目的大小之间的任何特定关系。一项的大小可能只是与填充的数组步幅不同。 我明白你的意思,但是“sizeof”有多大用处?你能用它做什么? (编辑:我刚刚注意到我的帖子中有一个错字,显然没有人注意到!那个定义是“大小”的定义,不是对齐的定义;我修复了它。) @Steve314:另一方面,假设结构包含int32short16char8,按顺序在前者具有 32 位的机器上结盟。如果结构被填充到 8 个字节,则可以使用两个 32 位操作来复制它,而不是一个 32 位、一个 16 位和一个 8 位操作。但是,如果该结构可以封装在另一个结构中,该结构在未使用的空间中包含一个字节,那么这种优化将不起作用。就个人而言,我认为 C 应该标准化一种强制结构对齐的方法,使其比标准更宽松或更严格,以便例如...【参考方案2】:

该标准很少提及填充和对齐。很少有保证。您唯一可以打赌的是,第一个元素位于结构的开头。之后...对齐和填充可以是任何东西。

【讨论】:

所以我的基于 sizeof 的代码是侥幸工作的?并且(更令人担忧)如果某些编译器决定奇怪(这意味着我应该替换我的数组步长计算)? 数组中类型 T 的开头与前一个开头的距离保证为 sizeof(T) 。在任何类结构中都没有提供相同的保证。 不仅仅是第一个元素;所有元素都以相同的顺序保证。所以不要将chars 与longs 交错以避免不必要的填充。 @Philip:保证具有相同可访问性的 AFAIK 元素是有序的,但不能保证具有不同可访问性的元素之间的关系,编译器可以选择重新组合具有相同可访问性的所有元素或交错他们或其他什么。 @Matthieu:啊,是用 C 而不是 C++ 思考的。对 C++ 了解得不够多,无法肯定地说。【参考方案3】:

所以把你的问题一分为二:

1.合法吗?

[5.3.3.2] 当应用于一个类时,结果 [sizeof() 运算符] 是该类对象中的字节数,包括任何所需的填充将该类型的对象放入数组中。

所以,不,不是。

2。那么,为什么不呢?

在这里,我只能推测。

2.1.指针算法变得更奇怪了 如果对齐是“数组元素之间”但不会影响大小,则 zthigns 会变得不必要地复杂,例如

(char *)(X+1) != ((char *)X) + sizeof(X)

(我有一种预感,即使没有上述声明,标准也隐含地要求这样做,但我无法证明)

2.2 简单 如果对齐影响大小,则可以通过查看单个类型来确定对齐和大小。考虑一下:

struct A    int x; char y;  
struct B   A left, right;   

按照目前的标准,我只需要知道 sizeof(A) 即可确定 B 的大小和布局。 使用您建议的替代方案,我需要了解 A 的内部结构。类似于您的example2:对于“更好的包装”, sizeof(example) 是不够的,您需要考虑示例的内部结构。

【讨论】:

好的,但我刚才说过 padded_sizeof 或 array_stride_of 会很方便。优点在于像问题中的 example2 这样的结构 - 对齐 4 的 6 个字节为每个项目提供 8 个字节,而不是两组 3 个字节填充的 12 个字节。有了足够大的数组,这 33% 的节省对于局部性和内存带宽以及空间节省很容易产生重大影响。 另外,在你的“右”成员上——如果那是一个 int 字段,你需要知道“内部细节”(对齐方式和大小)。为什么不对任何其他类型成员所做的结构成员做同样的事情呢?如果由于某种原因您在 2 字节整数上获得对齐 4(例如,某些特定于编译器的对齐设置功能),则该类型的两个相邻字段之间也需要一些额外的填充。 查看我的更新。我同意这对于像您的 example2 这样的情况会很好。 @Steve:但是一旦将 6 字节结构放入数组中,无论如何都必须对其进行填充。否则,数组的每个其他成员都将不对齐。所以唯一的区别是填充是在结构“内部”还是在结构之间。 C++ 选择了更简单、更一致的解决方案,即每个结构都必须具有大小,以便可以直接放入数组中。 @Steve:是的。如果这种额外的内存使用真的很重要,那么解决起来相当简单。但在大多数情况下,这是不值得的。你不妨抱怨bool 使用的内存比它需要的多 8 倍。但是,如果您允许结构根据何时以及如何使用而具有不同的大小,您可能会遇到各种奇怪和令人惊讶的行为。放置在数组中的结构的有效大小为 8 字节(指针必须指向更前面的 8 个字节才能指向下一个元素),但它作为结构的成员只会贡献 6 个字节跨度> 【参考方案4】:

我不确定这是否在实际的 C/C++ 标准中,我倾向于说这取决于编译器(只是为了安全起见)。然而,几个月前我有一个“有趣”的时间来解决这个问题,我必须将动态生成的 C 结构作为字节数组作为协议的一部分通过网络发送,以便与芯片通信。所有结构的对齐方式和大小必须与芯片上运行的代码中的结构一致,该代码是用 MIPS 架构的 GCC 变体编译的。我将尝试给出算法,它应该适用于 gcc 的所有变体(希望大多数其他编译器)。

所有基本类型,例如 charshortint 都与它们的大小对齐,并且它们与下一个可用位置对齐,无论父级的对齐方式如何。为了回答最初的问题,是的,总大小是对齐的倍数。

// size 8
struct 
    char A; //byte 0
    char B; //byte 1
    int C; //byte 4
;

尽管结构的对齐方式是 4 个字节,但字符仍然尽可能紧密地打包。

结构的对齐方式等于其成员的最大对齐方式

例子:

//size 4, but alignment is 2!
struct foo 
    char A; //byte 0
    char B; //byte 1
    short C; //byte 3


//size 6
struct bar 
    char A;         //byte 0
    struct foo B;   //byte 2

这也适用于工会,而且以一种奇怪的方式。联合的大小可以大于其成员的任何大小,这仅仅是由于对齐:

//size 3, alignment 1
struct foo 
    char A; //byte 0
    char B; //byte 1
    char C; //byte 2
;

//size 2, alignment 2
struct bar 
    short A; //byte 0
;

//size 4! alignment 2
union foobar 
    struct foo A;
    struct bar B;

使用这些简单的规则,您应该能够计算出您遇到的任何可怕的嵌套联合/结构的对齐方式/大小。这都是凭记忆,所以如果我错过了无法根据这些规则决定的极端情况,请告诉我!

【讨论】:

【参考方案5】:

似乎 C++03 标准没有说(或者我没有找到)对齐填充字节是否应该包含在对象表示中。

C99 标准规定结构类型或联合类型的“sizeof”包括内部和尾随填充,但我不确定是否所有对齐填充都包含在该“尾随填充”中。

现在回到你的例子。真的没有混淆。 sizeof(example) == 8 表示该结构确实需要 8 个字节来表示自身,包括尾随的 3 个填充字节。如果第二个结构中的 char 的偏移量为 6,它将覆盖m_Example 使用的空间。某种类型的布局是实现定义的,在整个实现中应该保持稳定。

仍然不确定p+1 是否等于(T*)((char*)p + sizeof(T))。我希望能找到答案。

【讨论】:

【参考方案6】:

标准说([dcl.array]

数组类型的对象包含一个连续分配的非空集合,由 N 个类型为 T 的子对象组成。

因此数组元素之间没有填充。

标准不要求在结构内部填充,但标准不允许任何其他对齐数组元素的方式。

【讨论】:

数组元素不需要特别对齐任何东西。编译器仅对齐它们以提高访问效率。所以编译器只需要在要对齐大于 1 的边界时才需要填充。 @Martin:标准要求 C++ 中的所有对象都必须对齐。当然,编译器可以任意决定每种类型只需要 1 字节对齐,但是大多数编译器使用 CPU 的底层对齐要求,并且无论编译器使用哪种对齐方式,它都必须确保对象尊重它,在数组和其他任何地方。 @Martin:标准不需要对齐,但硬件通常会这样做。并且标准规定,在数组中进行对齐的唯一方法是增加元素数据类型的大小,禁止在元素之间留下空洞。【参考方案7】:

5.3.3/2

当应用于一个类时,[sizeof] 的结果是该类的对象中的字节数,包括将该类型的对象放入数组中所需的任何填充。

所以是的,对象大小是其对齐方式的倍数。

【讨论】:

Ben Voigt 的引文和我的一样具有权威性。如果他们是矛盾的,那将是一个问题。【参考方案8】:

C++ 没有明确说明,但这是另外两个要求的结果:

首先,所有个对象都必须对齐。

3.8/1 说

T 类型对象的生命周期开始于 [...] 以正确对齐方式存储 并获得 T 类型的大小

和 3.9/5:

对象类型具有*对齐要求(3.9.1、3.9.2)。完整对象类型的alignment是一个实现定义的整数值,表示字节数;对象分配在满足其对象类型对齐要求的地址。

所以每个对象都必须根据其对齐要求对齐。

另一个要求是数组中的对象是连续分配的:

8.3.4/1:

数组类型的对象包含一组连续分配的非空N 子对象集T

对于要连续分配的数组中的对象,它们之间不能有填充。但是要使数组中的每个对象都正确对齐,必须填充每个单独的对象,以便紧跟在对象末尾之后的字节也可以很好地对齐。换句话说,对象的大小必须是其对齐方式的倍数。

【讨论】:

【参考方案9】:

可以生成一个 C 或 C++ typedef,其对齐方式不是其大小的倍数。这最近出现在 this bindgen bug 中。这是一个最小的示例,我将在下面调用test.c

#include <stdio.h>
#include <stdalign.h>

__attribute__ ((aligned(4))) typedef struct 
    char x[3];
 WeirdType;

int main() 
    printf("sizeof(WeirdType) = %ld\n", sizeof(WeirdType));
    printf("alignof(WeirdType) = %ld\n", alignof(WeirdType));
    return 0;

在我的 Arch Linux x86_64 机器上,gcc -dumpversion &amp;&amp; gcc test.c &amp;&amp; ./a.out 打印:

9.3.0
sizeof(WeirdType) = 3
alignof(WeirdType) = 4

类似clang -dumpversion &amp;&amp; clang test.c &amp;&amp; ./a.out 打印:

9.0.1
sizeof(WeirdType) = 3
alignof(WeirdType) = 4

将文件保存为test.cc 并使用g++/clang++ 会得到相同的结果。 (几年后的更新:我从 GCC 11.1.0 和 Clang 13.0.0 得到相同的结果。)

然而,值得注意的是,Windows 上的 MSVC不会似乎重现了任何类似的行为。

【讨论】:

最初的问题是关于结构而不是 typedef,所以假设不存在其他反例,超级迂腐的答案可能是“是的,sizeof(struct X) 始终是alignof(struct X) 的倍数。”但是,我认为大多数考虑这个问题的人实际上想知道它是否适用于任何类型,在这种情况下,答案显然是“不,sizeof(Y) 不一定是alignof(Y) 的倍数。” 有趣的事实:这种特殊的组合使得不可能创建这种类型的数组:WeirdType data[2]; 导致error: alignment of array elements is greater than element size

以上是关于结构的大小是不是需要是该结构对齐的精确倍数?的主要内容,如果未能解决你的问题,请参考以下文章

struct结构体内存大小

彻底搞清计算结构体大小和数据对齐原则

关于2的n次幂对齐

C结构体对齐

关于结构体大小一篇很详细的文章

结构体在内存中的存储方式