C语言中的数据结构对齐问题

8
我正在处理结构体并且有几个关于它们的问题。据我所知,结构体变量将按顺序放置在内存中。块(字)的长度取决于机器架构(32位-4字节,64位-8字节)。
假设我们有两个数据结构:
struct ST1 {
    char c1;
    short s;
    char c2;
    double d;
    int i;
};

"在记忆中它将是:"
32 bit - 20 bytes    
 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
------------------------------------------------------------------------------------------
 c1| PB| s | s | c1| PB| PB| PB| d | d | d  | d  | d  | d  | d  | d  | i  | i  | i  | i  |

64 bit - 24 bytes    | 20 | 21 | 22 | 23 |
previous sequence +  ---------------------
                     | PB | PB | PB | PB |

但是我们可以重新排列它,使这些数据适应机器字。像这样:
struct ST2 {
    double d;
    int i;
    short s;
    char c1;
    char c2;
};

在这种情况下,无论是32位还是64位,它都将以相同的方式表示(16个字节):
 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
----------------------------------------------------------------------
 d | d | d | d | d | d | d | d | i | i | i  | i  | s  | s  | ch1| ch2|

我有几个问题:
  • 这可能是一个猜测,但struct的主要规则是在开始定义更大的变量吗?
  • 我理解它不能用于独立变量。像char str[] = "Hello";
  • 填充字节,它的代码是什么?它在ASCII表中的某个位置吗?抱歉,我找不到。
  • 两个结构体,所有成员都由不同地址表示在内存中,它们可以不按顺序放置在内存中吗?
  • 这样的结构体:struct ST3 { char c1; char c2; char c3;} st3; 大小为3,如果我们将具有其他类型的成员添加到其中,它将被对齐。但为什么它没有被对齐呢?
5个回答

4
基本规则很简单:
  • 成员必须按顺序排列(除非在 C++ 中使用 private: public: ... 等部分)。
  • 允许在成员之间和最后添加填充。
这就是全部内容。其余的留给实现:类型占用的存储空间、填充量。通常可以期望在 ABI 或编译器中得到适当的文档,并且甚至有用于操作的工具。
在实践中,某些体系结构需要填充,例如 SPARC 需要对齐地址可被 4 整除的 32 位“int”。在其他体系结构上,这并不是必需的,但是未对齐的实体可能需要更长时间来处理,例如 80286 处理器需要额外的周期才能从奇数地址读取 16 位实体。(在我忘记之前:类型本身的表示形式也是不同的!)
通常情况下,对齐要求或最佳性能完全匹配:应将其与大小相同的边界对齐。一个良好的反例是 80 位浮点数(在某些编译器中可用作 double 或 long double),它们喜欢 8 或 16 字节对齐,而不是 10 字节。
为了调整填充,编译器通常会给出一个开关来设置默认值。这些从版本到版本都有所变化,因此在升级时最好将其考虑在内。在代码内部,还可以使用 gcc 中的 _attribute__(packed)MS 和其他许多工具中的 #pragma pack 来覆盖。显然,这些都是标准的扩展。
底线是,如果您想调整布局,请开始阅读所有编译器的文档,包括当前和未来的编译器,以了解它们如何工作并如何控制它们。可能还需要阅读目标平台的文档,具体取决于您为什么首先对布局感兴趣。
一般的动机之一是要在将原始内存写入文件并期望读回时具有稳定的布局,即使在使用不同编译器的不同平台上也是如此。直到新的平台类型进入场景,这是更容易的问题。
另一个动机是性能。这个问题要棘手得多,因为规则很快就会改变,而且效果很难立即预测。比如,在英特尔处理器上,基本的“未对齐”惩罚已经不存在了,相反,重要的是要在缓存行内。缓存行大小因处理器而异。同时,使用更多的填充可能会产生更好的个体效果,而完全打包的结构在缓存使用方面更经济。
有些操作需要适当地对齐,但是编译器并没有直接实施,您可能需要应用特殊的对齐指令(例如某些与 SSE 相关的内容)。
重复底线:不要猜测,确定您的目标并阅读适当的文档。(顺便说一句,对我来说阅读 SPARC、IA32 等架构手册非常有趣,收获颇丰。)

0

回答您的问题(忽略您非常漂亮的结构图片)

这就像是一个猜测,但结构体的主要规则是在开头定义更大尺寸的变量?

始终将需要最多对齐的内容放在最前面。例如,我不会首先放置一个 char[99]。通常情况下,指针、64位本机类型、32位本机类型等都可以按此规则排列,但如果您的结构包含其他结构的成员,则必须非常小心。

据我所知,它不能与独立变量一起使用。比如 char str[] = "Hello";

我真的不太理解这个问题。如果您在堆栈上定义了一个 char 数组,则它具有 char 对齐方式。如果您定义了一个 char 数组,后面跟着一个 int,则堆栈上可能会有填充,只是您找不到它。

填充字节,它的代码是什么?它是否在 ASCII 表中?抱歉,我找不到它。

它既没有代码也没有数据。它是编译器插入的填充,并且可能包含任何值,这些值在程序的同一或不同运行中的结构实例之间可能相同或不同。

“两个结构体的所有成员都在内存中用不同地址表示,并且它们可以在内存中非顺序地放置?” 我不太理解您的问题。您是否在询问编译器是否可以在结构体之间插入填充?如果不是,请澄清一下。因为这个回答可能没有多大帮助。 当编译器创建一个结构体时,它必须让您能够合理地创建这样的结构体数组。请考虑以下情况:
struct  S {
    int wibble;
    char wobble;
};

S stuff[2];

如果编译器在wobble后不插入3个字节的填充,对stuff[1].wobble的访问将无法正确对齐,这将导致某些硬件上崩溃(以及其他硬件上的可怕性能)。基本上,编译器必须确保在结构的末尾填充,以确保这样的结构数组的最对齐成员始终正确对齐。

这样的结构:struct ST3 { char c1; char c2; char c3;} st3;大小为3,我理解如果我们添加其他类型的成员,它将被对齐。但为什么它之前没有对齐呢?

你是说“为什么编译器不把它放在正确对齐的位置吗?”因为语言不允许。编译器不能重新排列结构的成员。它只能插入填充。


0

结构体(和类)成员的对齐方式取决于平台,但也取决于编译器。将成员对齐到其大小的原因是为了提高性能。使所有整数类型与其大小对齐可以减少内存访问。

通常情况下,您可以强制编译器减少对齐,但除了特定原因(例如,用于不同平台之间的数据兼容性,如通信数据)外,这并不是一个好主意。在Visual C++中存在#pragma pack,例如:

#pragma pack(1)
struct ST1 {
    char c1;
    short s;
    char c2;
    double d;
    int i;
};

assert(sizeof(ST1) == 16);

但正如我之前所说,这通常不是一个好主意。

请记住,编译器不仅在某些字段后添加填充字节。它还确保结构体分配在内存中,以便所有字段都正确对齐。我的意思是,在您的ST1示例中,因为更大的字段类型是double,编译器将确保d字段在8个字节对齐(除非使用#pragma pack或类似选项):

ST1 st1;

assert(&st1.d % 8 == 0);

关于你的问题:
  • 如果你想节省空间,按大小顺序排列字段是一个好技巧,先写最大的。在组合结构体的情况下,使用内部结构体中更大字段的大小,而不是结构体的大小。
  • 它适用于独立变量。但编译器可以对内存中的变量进行排序(与结构体和类的成员相反)。
例如:
short   s[27];
int32_t i32[34];
int64_t i64[45];

assert(s % 2 == 0);
assert(i32 % 4 == 0);
assert(i64 % 8 == 0);
  • 填充字节可以包含任何内容。通常情况下是已初始化数据(至少您进行了初始化)。有时编译器为了调试目的可能会包含特定的字节模式。
  • 关于所有成员在内存中用不同地址表示的结构:抱歉,我不太明白您的问题。
  • 标准C++规定,结构体/类的地址必须与其第一个字段的地址相同。因此,在c3之后只能填充,而c1之前永远不可能存在填充。

从N3337(C++11)[9.2 class.menu,p.20]:

使用reinterpret_cast适当转换的指向标准布局结构体对象的指针指向其初始成员(或如果该成员是位域,则指向其中所在的单元),反之亦然。 [注意:因此,标准布局结构体对象中可能存在未命名的填充,但不会出现在其开头,因为需要实现适当的对齐。 —end note]


0

对于基于英特尔架构的gcc编译器,访问(读/写)奇数内存地址需要更多的指令和周期。因此,需要添加填充以实现偶数内存地址。


-1

注意,你不能确定你的变量是否对齐(但通常是这样的)。 如果你使用GCC,你可以使用属性 packed 来确保你的数据对齐。

例如:

struct foo {
    char c;
    int x;
} __attribute__((packed));

据我理解,它不能与独立变量一起使用。比如 char str[] = "Hello";?
这个表将在您的内存中对齐。

2
等等,什么?使用packed将消除填充,可能会强制成员不正确地对齐,这即使在x86上也有影响(例如,只有当double正确对齐时才是原子访问)。 - Christoph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接