丢失的C语言艺术——结构体打包

Table of Contents

  1. Who should read this
  2. Why I wrote it
  3. Alignment requirements
  4. Padding
  5. Structure alignment and padding
  6. Bitfields
  7. Structure reordering
  8. Awkward scalar cases
  9. Readability and cache locality
  10. Other packing techniques
  11. Overriding alignment rules
  12. Tools
  13. Proof and exceptional cases

1. Who should read this

本文介绍了一种减少具有类似 C 结构的编译语言中的程序内存占用的技术 - 手动重新打包这些声明以减小大小。要阅读它,您需要 C 编程语言的基础知识。
如果您打算为内存受限的嵌入式系统或操作系统内核编写代码,则需要了解此技术。如果您使用的应用程序数据集太大以至于您的程序经常达到内存限制,那么它会很有用。很高兴知道在任何应用程序中您确实非常关心优化内存带宽的使用并最大限度地减少缓存行丢失。
最后,了解这项技术是通向其他深奥 C 主题的门户。在掌握这些规则之前,您不是高级 C 程序员。除非您自己编写了这份文档并能够明智地批评它,否则您还不是 C 大师。
该文档最初的标题中包含“C”,但其中几乎所有内容也适用于 C++。

2. Why I wrote it

这个网页的存在是因为在 2013 年末,我发现自己大量应用了一种优化技术,这种技术是我二十多年前学到的,但此后就很少使用了。
我需要减少使用数千(有时是数十万)C 结构体实例的程序的内存占用。该程序是 cvs-fast-export,问题是它在大型存储库上因内存不足错误而崩溃。
在这种情况下,有一些方法可以通过仔细地重新排列结构成员的顺序来显着减少内存使用量。这可以带来巨大的收益 - 在我的例子中,我能够将工作集大小减少大约 40%,使程序能够处理更大的存储库而不会崩溃。
但当我工作并思考我在做什么时,我开始意识到我所使用的技术在最近几天已经被遗忘了一半以上。一项小型网络研究证实,程序员似乎不再谈论它,至少在搜索引擎可以看到他们的地方不再谈论它。维基百科上有几个条目涉及到这个主题,但我发现没有人全面涵盖它。
实际上有一些并不愚蠢的原因。计算机科学课程(正确地)引导人们远离微观优化,转向寻找更好的算法。机器资源价格的暴跌使得压缩内存使用的必要性降低了。过去,黑客学习如何做到这一点的方式是通过尝试奇怪的硬件架构——现在这种经历已经不太常见了。
但该技术在重要情况下仍然有价值,并且只要内存有限,它就会发挥作用。本文档旨在使程序员不必重新发现该技术,以便他们可以将精力集中在更重要的事情上。

3. Alignment requirements

首先要了解的是,在现代处理器上,编译器在内存中布置基本数据类型的方式受到限制,以便加快内存访问速度。我们的示例是用 C 语言编写的,但任何编译语言都会在相同的约束下生成代码。
有一大类现代 ISA(指令集架构),这些约束导致了相同的布局。这些 ISA 包括 Intel、ARM 和 RISC-V;我将这些称为“普通”ISA。
普通 ISA 上基本 C 数据类型的存储通常不会从内存中的任意字节地址开始。相反,除 char 之外的每种类型都有对齐要求;字符可以从任何字节地址开始,但 2 字节短整型必须从偶数地址开始,4 字节整型或浮点必须从可被 4 整除的地址开始,8 字节长整型或双精度必须从可被 8 整除的地址开始. 签名或未签名没有区别。
行话是,普通 ISA 上的基本 C 类型是自对齐的。指针,无论是 32 位(4 字节)还是 64 位(8 字节)都是自对齐的。
自对齐使访问速度更快,因为它有助于生成类型化数据的单指令获取和放入。另一方面,如果没有对齐约束,代码最终可能必须进行两次或多次跨越机器字边界的访问。字符是一种特殊情况;它们在同一个机器字内的任何地方都同样昂贵。这就是为什么他们没有首选的对齐方式。
我说“在现代处理器上”是因为在一些较旧的处理器上强制 C 程序违反对齐规则(例如,通过将奇数地址转换为 int 指针并尝试使用它)不仅会减慢代码速度,还会导致非法指令故障。例如,Sun SPARC 芯片上就是这种行为。事实上,只要有足够的决心并在处理器上设置正确的 (e18) 硬件标志,您仍然可以在 x86 上触发此操作。
此外,自对准并不是唯一可能的规则。从历史上看,一些处理器(尤其是那些缺少桶式移位器的处理器)具有更严格的限制。如果您从事嵌入式系统,您可能会被潜伏在灌木丛中的其中一个绊倒。请注意这是可能的。
摩托罗拉 68020 及其后继产品是一个令人好奇且具有说明性的例外。这些是面向字的 32 位机器——也就是说,快速访问的底层粒度是 16 位。编译器可以在 16 位边界上启动结构,而不会影响速度,即使第一个成员是 32 位标量。因此,只有奇数字节长度的字符字段才会导致填充。
从 2014 年初首次撰写到 2016 年底,本节以有关奇怪架构的其他警告结束。在那段时间里,我从使用 NTP 参考实现的源代码中学到了一些相当令人放心的东西。它通过将数据包从线路上直接读取到内存中来进行数据包分析,其余代码将其视为一个结构,依赖于最小自对齐填充的假设 - 或在 690x0 等奇怪情况下零填充的假设。
有趣的消息是,几十年来,NTP 显然在各种硬件、操作系统和编译器上都摆脱了这种情况,不仅包括 Unix,还包括 Windows 变体。这表明具有除自对齐之外的填充规则的平台要么不存在,要么仅限于如此专门的领域,以至于它们永远不是 NTP 服务器或客户端。

4. Padding

现在我们将看一个内存中变量布局的简单示例。考虑 C 模块顶层的以下一系列变量声明:

char *p;
char c;
int x;

如果您对数据对齐一无所知,您可能会假设这三个变量将占用内存中连续的字节范围。也就是说,在 32 位机器上,4 个字节的指针后面会紧跟着 1 个字节的 char,然后紧接着是 4 个字节的 int。 64 位机器的不同之处仅在于指针为 8 个字节。
事实上,静态变量的分配顺序就是它们的源顺序的隐藏假设并不一定有效; C 标准没有强制要求它。我将忽略这个细节,因为(a)隐藏的假设通常是正确的,并且(b)讨论填充和包装外部结构的实际目的是让你为它们内部发生的情况做好准备。
以下是实际发生的情况(在 x86 或 ARM 或任何其他具有自对齐类型的设备上)。 p 的存储从自对齐 4 或 8 字节边界开始,具体取决于机器字大小。这是指针对齐——最严格的对齐方式。
c 的存储紧随其后。但是 x 的 4 字节对齐要求迫使布局中出现间隙;结果好像还有第四个中间变量,如下所示:

char p; / 4 or 8 bytes /
char c; /
1 byte /
char pad[3]; /
3 bytes /
int x; /
4 bytes */

pad[3] 字符数组表示该结构中存在三个字节的浪费空间。对此的老式术语是“slop”。填充位的值未定义;特别是不保证它们会被归零。

比较一下如果 x 是 2 字节短值会发生什么:

char *p;
char c;
short x;

在这种情况下,实际的布局将是这样的:

char p; / 4 or 8 bytes /
char c; /
1 byte /
char pad[1]; /
1 byte /
short x; /
2 bytes */

另一方面,如果 x 在 64 位机器上是 long

char *p;
char c;
long x;

我们最终得到这样的结果:

char p; / 8 bytes /
char c; /
1 byte
char pad[7]; /* 7 bytes /
long x; /
8 bytes */

如果您仔细观察,您现在可能想知道较短的变量声明首先出现的情况:

char c;
char *p;
int x;

如果实际的内存布局是这样写的:

char c;
char pad1[M];
char *p;
char pad2[N];
int x;

关于M和N我们能说什么?
首先,在这种情况下 N 将为零。 x 的地址紧随 p 之后,保证是指针对齐的,这永远不会比 int 对齐严格。
M 的值不太可预测。如果编译器碰巧将 c 映射到机器字的最后一个字节,则下一个字节(p 的第一个字节)将是下一个字节的第一个字节,并且正确地进行指针对齐。 M 为零。
c 更有可能被映射到机器字的第一个字节。在这种情况下,M 将是确保 p 具有指针对齐所需的任何填充 - 在 32 位机器上为 3,在 64 位机器上为 7。
中间情况是可能的。 M 可以是 0 到 7 之间的任何值(32 位上是 0 到 3),因为 char 可以从机器字中的任何字节边界开始。
如果您想让这些变量占用更少的空间,可以通过在原始序列中将 x 与 c 交换来达到此效果。

char p; / 8 bytes /
long x; /
8 bytes /
char c; /
1 byte

通常,对于 C 程序中的少量标量变量,通过更改声明顺序获得的几个字节并不会节省足够的时间。当应用于非标量变量(尤其是结构体)时,该技术变得更加有趣。
在我们开始之前,让我们处理一下标量数组。在自对齐类型的平台上,char/short/int/long/pointer 数组没有内部填充;每个成员都会在下一个成员的末尾自动进行自对齐。
所有这些规则和示例都映射到 Go 结构,以及具有“repr©”属性的 Rust 结构,仅进行语法更改。
在下一节中,我们将看到结构数组不一定如此。

5. 结构对齐和填充

一般来说,结构实例将具有其最宽标量成员的对齐方式。编译器这样做是确保所有成员自我对齐以实现快速访问的最简单方法。
此外,在 C(以及 Go 和 Rust)中,结构体的地址与其第一个成员的地址相同 - 没有前导填充。在 C++ 中这可能不是真的;
(当您对此类事情有疑问时,ANSI C 提供了一个 offsetof() 宏,可用于读出结构成员偏移量。)
考虑这个结构:

struct foo1 {
char *p;
char c;
long x;
};

假设是 64 位机器,struct foo1 的任何实例都将具有 8 字节对齐。其中一个的内存布局看起来并不令人惊讶,如下所示:

struct foo1 {
char p; / 8 bytes /
char c; /
1 byte
char pad[7]; /* 7 bytes /
long x; /
8 bytes */
};

它的布局完全就像这些类型的变量是单独声明的一样。但如果我们把 c 放在第一位,那就不再正确了。

struct foo2 {
char c; /* 1 byte /
char pad[7]; /
7 bytes */
char p; / 8 bytes /
long x; /
8 bytes */
};

如果成员是单独的变量,则 c 可以从任何字节边界开始,并且 pad 的大小可能会有所不同。因为 struct foo2 具有其最宽成员的指针对齐,所以这不再可能。现在 c 必须指针对齐,并且随后的 7 个字节的填充被锁定。

现在我们来谈谈结构上的尾随填充。为了解释这一点,我需要介绍一个基本概念,我将其称为结构的跨步地址。它是结构数据后面的第一个地址,与结构具有相同的对齐方式。

尾随结构填充的一般规则是这样的:编译器的行为就像结构将尾随填充扩展到其跨步地址一样。该规则控制 sizeof() 将返回的内容。
在 64 位 x86 或 ARM 计算机上考虑此示例:

struct foo3 {
char p; / 8 bytes /
char c; /
1 byte */
};
struct foo3 singleton;
struct foo3 quad[4];

你可能认为 sizeof(struct foo3) 应该是 9,但实际上是 16。步幅地址是 (&p)[2] 的地址。因此,在四元组中,每个成员都有 7 个字节的尾部填充,因为每个后续结构的第一个成员都希望在 8 字节边界上自对齐。内存布局就好像结构是这样声明的:

struct foo3 {
char p; / 8 bytes /
char c; /
1 byte */
char pad[7];
};

为了进行对比,请考虑以下示例:

struct foo4 {
short s; /* 2 bytes /
char c; /
1 byte */
};

因为 s 只需要 2 字节对齐,因此步幅地址仅在 c 之后一个字节,并且 struct foo4 整体上只需要一个字节的尾部填充。它将被布置成这样:

struct foo4 {
short s; /* 2 bytes /
char c; /
1 byte */
char pad[1];
};

sizeof(struct foo4) 将返回 4。
这是最后一个重要细节:如果您的结构具有结构成员,则内部结构也希望具有最长标量的对齐方式。假设你这样写:

struct foo5 {
char c;
struct foo5_inner {
char *p;
short x;
} inner;
};

内部结构中的 char *p 成员强制外部结构与内部结构一样进行指针对齐。在 64 位机器上的实际布局将是这样的:

struct foo5 {
char c; /* 1 byte*/
char pad1[7]; /* 7 bytes */
struct foo5_inner {
char p; / 8 bytes /
short x; /
2 bytes /
char pad2[6]; /
6 bytes */
} inner;
};

这种结构向我们暗示了重新包装结构可能带来的节省。 24 个字节中,有 13 个是填充字节。这浪费了超过 50% 的空间!

6. Bitfields

现在让我们考虑一下 C 位域。它们使您能够声明小于字符宽度的结构字段,低至 1 位,如下所示:

struct foo6 {
short s;
char c;
int flip:1;
int nybble:4;
int septet:7;
};

关于位域需要了解的是,它们是通过字级和字节级掩码以及在机器字上操作的旋转指令来实现的,并且不能跨越字边界。 C99 保证位字段将尽可能紧密地打包,前提是它们不跨越存储单元边界 (6.7.2.1 #10)。
此限制在 C11 (6.7.2.1p11) 和 C++14 ([class.bit]p1) 中放宽;这些修订实际上并不要求 struct foo9 为 64 位而不是 32 位;一个位字段可以跨越多个分配单元,而不是开始一个新的分配单元。由实施决定; GCC 将其留给 ABI,对于 x64 来说,这确实阻止它们共享分配单元。
假设我们在 32 位机器上,C99 规则意味着布局可能如下所示:

struct foo6 {
short s; /* 2 bytes /
char c; /
1 byte /
int flip:1; /
total 1 bit /
int nybble:4; /
total 5 bits /
int pad1:3; /
pad to an 8-bit boundary /
int septet:7; /
7 bits /
int pad2:25; /
pad to 32 bits */
};

但这并不是唯一的可能性,因为 C 标准没有规定位是从低到高分配的。所以布局可能如下所示:

struct foo6 {
short s; /* 2 bytes /
char c; /
1 byte /
int pad1:3; /
pad to an 8-bit boundary /
int flip:1; /
total 1 bit /
int nybble:4; /
total 5 bits /
int pad2:25; /
pad to 32 bits /
int septet:7; /
7 bits */
};

也就是说,填充可以在有效负载位之前而不是在有效负载位之后。
另请注意,与正常结构填充一样,填充位不保证为零; C99提到了这一点。
请注意,位字段的基本类型被解释为符号性,但不一定是大小。是否支持“短翻转:1”或“长翻转:1”以及这些基本类型是否更改字段打包到的存储单元的大小取决于实现者。
请谨慎操作,并使用 -Wpadded 检查是否可用(例如在 clang 下)。特殊硬件上的编译器可能会以令人惊讶的方式解释 C99 规则;较旧的编译器可能不太遵循它们。
位域不能跨越机器字边界的限制意味着,虽然以下结构中的前两个按照您的预期打包为一个和两个 32 位字,但第三个结构 (struct foo9) 在 C99 中占用三个 32 位字,其中最后仅使用一位。

struct foo7 {
int bigfield:31; /* 32-bit word 1 begins /
int littlefield:1;
};
struct foo8 {
int bigfield1:31; /
32-bit word 1 begins /*
int littlefield1:1;
int bigfield2:31; /* 32-bit word 2 begins /
int littlefield2:1;
};
struct foo9 {
int bigfield1:31; /
32-bit word 1 begins /
int bigfield2:31; /
32-bit word 2 begins /
int littlefield1:1;
int littlefield2:1; /
32-bit word 3 begins */
};

同样,C11 和 C++14 可能会将 foo9 打包得更紧,但指望这一点可能是不明智的。
另一方面,如果机器有这些,struct foo8 将适合单个 64 位字。

7. Structure reordering

现在您已经知道编译器如何以及为何在结构中和结构后插入填充,我们将研究如何消除溢出。这就是结构打包的艺术。
首先要注意的是,溢出只发生在两个地方。一种是绑定到较大数据类型(具有更严格的对齐要求)的存储遵循绑定到较小数据类型的存储。另一种是结构在其跨步地址之前自然结束,需要填充以便下一个结构能够正确对齐。
消除倾斜的最简单方法是通过减少对齐来重新排序结构构件。也就是说:让所有指针对齐的子字段都放在第一位,因为在 64 位机器上它们将是 8 个字节。然后是 4 字节整数;然后是 2 字节的短裤;然后是字符字段。
例如,考虑这个简单的链表结构:

struct foo10 {
char c;
struct foo10 *p;
short x;
};

将隐含的斜率明确化后,如下所示:

struct foo10 {
char c; /* 1 byte /
char pad1[7]; /
7 bytes */
struct foo10 p; / 8 bytes /
short x; /
2 bytes /
char pad2[6]; /
6 bytes */
};

那是 24 个字节。如果我们按大小重新排序,我们会得到:

struct foo11 {
struct foo11 *p;
short x;
char c;
};

考虑到自对齐,我们发现没有数据字段需要填充。这是因为具有更严格对齐的(较长)字段的步幅地址始终是具有不太严格要求的(较短)字段的有效对齐起始地址。所有重新包装的结构实际上需要的是尾随填充:

struct foo11 {
struct foo11 p; / 8 bytes /
short x; /
2 bytes /
char c; /
1 byte /
char pad[5]; /
5 bytes */
};

我们的重新打包转换将大小从 24 字节减少到 16 字节。这看起来可能不是很多,但是假设您有一个包含 200K 个这样的链接列表?节省的成本加起来很快 - 特别是在内存受限的嵌入式系统或必须保持常驻的操作系统内核的核心部分。

请注意,重新订购并不能保证节省费用。将此技术应用于之前的示例 struct foo5,我们得到:

struct foo12 {
struct foo5 {
char p; / 8 bytes /
short x; /
2 bytes /
} inner;
char c; /
1 byte*/
};

写出填充后,这是

struct foo12 {
struct foo5 {
char p; / 8 bytes /
short x; /
2 bytes /
char pad[6]; /
6 bytes /
} inner;
char c; /
1 byte*/
char pad[7]; /* 7 bytes */
};

它仍然是 24 个字节,因为 c 无法返回内部结构的尾部填充。为了获得这种收益,您需要重新设计数据结构。

奇怪的是,通过增加大小来严格排序结构字段也可以最大限度地减少填充。您可以按任意顺序最小化填充,其中 (a) 任何一种大小的所有字段都位于连续范围内(完全消除它们之间的填充),并且 (b) 这些范围之间的间隙使得两侧的大小为彼此之间的差异尽可能少加倍。通常这意味着一侧根本没有填充。
甚至更一般的最小填充顺序也是可能的。例子:

struct foo13 {
int32_t i;
int32_t i2;
char octet[8];
int32_t i3;
int32_t i4;
int64_t l;
int32_t i5;
int32_t i6;
};

该结构在自对齐规则下具有零填充。找出原因是增进理解的有用练习。
自从发布本指南的第一个版本以来,有人问我,如果重新排序以最小化溢出如此简单,为什么 C 编译器不会自动执行此操作。答案:C 是一种最初设计用于编写操作系统和其他接近硬件的代码的语言。自动重新排序会干扰系统程序员布置与内存映射设备控制块的字节和位级布局完全匹配的结构的能力。
Go 遵循 C 哲学并且不会重新排序字段。 Rust 做出了相反的选择;默认情况下,其编译器可能会重新排序结构字段。

8. Awkward scalar cases

使用枚举类型而不是 #defines 是一个好主意,因为符号调试器具有可用的这些符号并且可以显示它们而不是原始整数。但是,虽然枚举保证与整型兼容,但 C 标准并未指定枚举要使用哪种基础整型。
重新打包结构时请注意,虽然枚举类型变量通常是整数,但这是依赖于编译器的;默认情况下,它们可以是空头、长头,甚至是字符。您的编译器可能有一个编译指示或命令行选项来强制指定大小。
长双型也是类似的问题点。有些 C 平台以 80 位实现,有些以 128 位实现,有些 80 位平台将其填充到 96 或 128 位。
在这两种情况下,最好使用 sizeof() 来检查存储大小。
最后,在 x86 Linux 下,双精度有时是自对齐规则的例外;即使独立的双精度变量具有 8 字节自对齐,8 字节双精度可能只需要结构内的 4 字节对齐。这取决于编译器和选项。

9. Readability and cache locality

虽然按尺寸重新排序是消除溢出的最简单方法,但这不一定是正确的方法。还有两个问题:可读性和缓存局部性。
程序不仅仅是与计算机的通信,也是与其他人的通信。即使(或尤其是!)当通信的受众只是未来的你时,代码的可读性也很重要。
对结构进行笨拙、机械的重新排序可能会损害可读性。如果可能,最好对字段重新排序,使它们保持在连贯的组中,并且语义相关的数据片段保持紧密在一起。理想情况下,结构的设计应该传达程序的设计。
当您的程序频繁访问一个结构或结构的一部分时,如果访问往往适合缓存行(当处理器被告知获取块内的任何单个地址时由处理器获取的内存块),则对性能很有帮助。在 64 位 x86 上,高速缓存行是从自对齐地址开始的 64 字节;在其他平台上它通常是 32 字节。
为了保持可读性,您应该做的事情 - 在相邻字段中对相关和共同访问的数据进行分组 - 也可以提高缓存行局部性。这些都是通过了解代码的数据访问模式进行智能重新排序的原因。
如果您的代码从多个线程并发访问结构,则会出现第三个问题:缓存行弹跳。为了最大限度地减少昂贵的总线流量,您应该安排数据,以便读取来自一个缓存行,而写入则转到更紧密的循环中的另一缓存行。
是的,这有时与之前有关将相关数据分组到同一缓存行大小的块中的指导相矛盾。多线程很难。缓存行弹跳和其他多线程优化问题是非常高级的主题,值得单独编写一个完整的教程。我在这里能做的就是让您意识到这些问题的存在。

10. Other packing techniques

重新排序与其他精简结构的技术结合使用效果最佳。例如,如果结构中有多个布尔标志,请考虑将它们减少为 1 位位字段,并将它们打包到结构中的某个位置,否则会出现混乱。
为此,您将承受较小的访问时间损失 - 但如果它将工作集压缩得足够小,则该损失将被避免缓存未命中所带来的收益所淹没。
更一般地说,寻找缩短数据字段大小的方法。例如,在 cvs-fast-export 中,我应用的一个挤压是利用 1982 年之前不存在 RCS 和 CVS 存储库的知识。我放弃了 64 位 Unix time_t(1970 年初的零日期) 32 位时间偏移​​量 1982-01-01T00:00:00;这将涵盖日期到 2118 年。(注意:如果您使用这样的技巧,请在设置字段时进行边界检查以防止出现讨厌的错误!)
每一次这样的字段缩短不仅会减少结构的显式大小,还可能消除溢出和/或为从字段重新排序中获得收益创造额外的机会。这种效应的良性级联并不难触发。
最危险的包装形式是使用联合。如果您知道结构中的某些字段永远不会与某些其他字段组合使用,请考虑使用联合来使它们共享存储。但要格外小心,并通过回归测试验证您的工作,因为如果您的生命周期分析哪怕是轻微错误,您都会遇到从崩溃到(更糟糕的)微妙数据损坏的错误。

11. Overriding alignment rules

有时,您可以通过使用编译指示(通常是#pragma pack)强制编译器不使用处理器的正常对齐规则。 GCC 和 clang 有一个“packed”属性,您可以将其附加到各个结构声明中; GCC 有一个用于整个编译的 -fpack-struct 选项。
不要随意这样做,因为它会强制生成更昂贵且更慢的代码。通常,使用我在这里描述的技术,您可以节省尽可能多或几乎同样多的内存。
#pragma pack 唯一真正令人信服的原因是,如果您必须将 C 数据布局与某种位级硬件或协议要求(例如内存映射硬件端口)完全匹配,并且需要违反正常对齐才能工作。如果你处于这种情况,并且你还不知道我在这里写的其他所有内容,那么你就有很大的麻烦了,我祝你好运。

12. Tools

clang 编译器有一个 -Wpadded 选项,可以使其生成有关对齐孔和填充的消息。某些版本还具有未记录的 -fdump-record-layouts 选项,可以生成更多信息。
如果您使用 C11,您可以部署 static_assert 来检查您对类型和结构大小的假设。例子:

#include <assert.h>
struct foo4 {
short s; /* 2 bytes /
char c; /
1 byte */
};
static_assert(sizeof(struct foo4) == 4, “Check your assumptions");

我自己没有使用过,但一些受访者对一个名为 pahole 的程序评价很高。该工具与编译器配合生成有关结构的报告,描述填充、对齐和缓存行边界。这曾经是一个独立的 C 程序,但现在已无人维护;名为 pahole 的脚本现在随 gdb 一起提供,这就是您应该使用的脚本。
我收到一份报告,称名为“PVS Studio”的专有代码审核工具可以检测结构打包机会。

13. Proof and exceptional cases

您可以下载一个小程序的源代码,该程序演示了上面关于标量和结构大小的断言。它是packtest.c。
如果你仔细观察编译器、选项和不寻常硬件的足够奇怪的组合,你会发现我所描述的一些规则有例外。当你回到旧的处理器设计时,它们会变得更加常见。
除了了解这些规则之外,下一个层次是了解这些规则将如何以及何时被打破。在我了解这些的那些年里(20世纪80年代初),我们谈到那些没有意识到这一点的人是“全世界的VAX”综合症的受害者。请记住,并非所有世界都是香草。

#include <stdio.h>
#include <stdbool.h>
#include <stddef.h>

/* The expected sizes in these comments assime a 64-bit machine */

struct foo1 {
    char *p;
    char c;
    long x;
};

struct foo2 {
    char c;      /* 1 byte */
    char pad[7]; /* 7 bytes */
    char *p;     /* 8 bytes */
    long x;      /* 8 bytes */
};

struct foo3 {
    char *p;     /* 8 bytes */
    char c;      /* 1 byte */
};

struct foo4 {
    short s;     /* 2 bytes */
    char c;      /* 1 byte */
};

struct foo5 {
    char c;
    struct foo5_inner {
        char *p;
        short x;
    } inner;
};

struct foo6 {
    short s;
    char c;
    int flip:1;
    int nybble:4;
    int septet:7;
};

struct foo7 {
    int bigfield:31;
    int littlefield:1;
};

struct foo8 {
    int bigfield1:31;
    int littlefield1:1;
    int bigfield2:31;
    int littlefield2:1;
};

struct foo9 {
    int bigfield1:31;
    int bigfield2:31;
    int littlefield1:1;
    int littlefield2:1;
};

struct foo10 {
    char c;
    struct foo10 *p;
    short x;
};

struct foo11 {
    struct foo11 *p;
    short x;
    char c;
};

struct foo12 {
    struct foo12_inner {
        char *p;
        short x;
    } inner;
    char c;
};

int main(int argc, char *argv)
{
    printf("sizeof(char *)        = %zu\n", sizeof(char *));
    printf("sizeof(long)          = %zu\n", sizeof(long));
    printf("sizeof(int)           = %zu\n", sizeof(int));
    printf("sizeof(short)         = %zu\n", sizeof(short));
    printf("sizeof(char)          = %zu\n", sizeof(char));
    printf("sizeof(float)         = %zu\n", sizeof(float));
    printf("sizeof(double)        = %zu\n", sizeof(double));
    printf("sizeof(struct foo1)   = %zu\n", sizeof(struct foo1));
    printf("sizeof(struct foo2)   = %zu\n", sizeof(struct foo2));
    printf("sizeof(struct foo3)   = %zu\n", sizeof(struct foo3));
    printf("sizeof(struct foo4)   = %zu\n", sizeof(struct foo4));
    printf("sizeof(struct foo5)   = %zu\n", sizeof(struct foo5));
    printf("sizeof(struct foo6)   = %zu\n", sizeof(struct foo6));
    printf("sizeof(struct foo7)   = %zu\n", sizeof(struct foo7));
    printf("sizeof(struct foo8)   = %zu\n", sizeof(struct foo8));
    printf("sizeof(struct foo9)   = %zu\n", sizeof(struct foo9));
    printf("sizeof(struct foo10)   = %zu\n", sizeof(struct foo10));
    printf("sizeof(struct foo11)   = %zu\n", sizeof(struct foo11));
    printf("sizeof(struct foo12)   = %zu\n", sizeof(struct foo12));

    if (sizeof(struct foo3) == 16) {
	puts("This looks like a 64-bit machine.");
    } else if (sizeof(struct foo3) == 6) {
	puts("This looks like a 32-bit machine.");
    } else {
	puts("Huh? The word size of this mahine is not obvious");
    }

    if ((offsetof(struct foo1, x) % sizeof(long)) == 0) {
	puts("Self-alignment seems to be required.");
    } else {
	puts("Self-alignment test of type long failed.");
    }
}
  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂的码泰君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值