设为首页 加入收藏

TOP

C语言编程笔记丨失落的C语言结构体封装艺术(一)
2019-03-30 00:08:19 】 浏览:279
Tags:语言编程 笔记 失落 语言 结构 封装 艺术

1. 谁该阅读这篇文章

本文是关于削减C语言程序内存占用空间的一项技术——为了减小内存大小而手工重新封装C结构体声明。你需要C语言的基本知识来读懂本文。

如果你要为内存有限制的嵌入式系统、或者操作系统内核写代码,那么你需要懂这项技术。如果你在处理极大的应用程序数据集,以至于你的程序常常达到内存的界限时,这项技术是有帮助的。在任何你真的真的需要关注将高速缓存行未命中降到最低的应用程序里,懂得这项技术是很好的。

最后,理解该技术是一个通往其他深奥的C语言话题的入口。直到你掌握了它,你才成为一个高端的C程序员。直到你可以自己写出这篇文档并且可以理智地评论它,你才成为一位C语言大师。

 

2. 我为什么写这篇文章

本文之所以存在,是因为在2013年底,我发现我自己在大量使用一项C语言的优化技术,我早在二十多年前就已经学会了该技术,不过在那之后并没怎么使用过。

我需要减小一个程序的内存占用空间,它用了几千——有时是几十万个——C结构体的实例。这个程序是cvs-fast-export,而问题在于处理巨大的代码库时,它曾因内存耗尽的错误而濒临崩溃。

在这类情况下,有好些办法能极大地减少内存使用的,比如小心地重新安排结构体成员的顺序之类的。这可以获得巨大的收益——在我的事例中,我能够减掉大约40%的工作区大小,使得程序能够在不崩溃的情况下处理大得多的代码库。

当我解决这个问题,并且回想我所做的工作时,我开始发现,我在用的这个技术现今应被忘了大半了。一个网络调查确认,C程序员好像已经不再谈论该技术了,至少在搜索引擎可以看到的地方不谈论了。有几个维基百科条目触及了这个话题,但是我发现没人能全面涵盖。

实际上这个现象也是有合理的理由的。计算机科学课程(应当)引导人们避开细节的优化而去寻找更好的算法。机器资源价格的暴跌已经使得压榨内存用量变得不那么必要了。而且,想当年,骇客们曾经学习如何使用该技术,使得他们在陌生的硬件架构上撞墙了——现在已经不太常见的经历。

但是这项技术仍然在重要的场合有价值, 并且只要内存有限,就能永存。本文目的就是让C程序员免于重新找寻这项技术,而让他们可以集中精力在更重要的事情上。

 

3. 对齐要求(Alignment Requirement)

要明白的第一件事是,在现代处理器上,你的C编译器在内存里对基本的C数据类型的存放方式是受约束的,为的是内存访问更快。

在x86或者ARM处理器上,基本的C数据类型的储存一般并不是起始于内存中的任意字节地址。而是,每种类型,除了字符型以外,都有对齐要求;字符可以起始于任何字节地址,但是2字节的短整型必须起始于一个偶数地址,4字节整型或者浮点型必须起始于被4整除的地址,以及8字节长整型或者双精度浮点型必须起始于被8整除的地址。带符号与不带符号之间没有差别。

这个的行话叫:在x86和ARM上,基本的C语言类型是自对齐(self-aligned)的。指针,无论是32位(4字节)亦或是64位(8字节)也都是自对齐的。

自对齐使得访问更快,因为它使得一条指令就完成对类型化数据的取和存操作。没有对齐的约束,反过来,代码最终可能会不得不跨越机器字的边界做两次或更多次访问。字符是特殊的情况;无论在一个单机器字中的何处,存取的花费都是一样的。那就是为什么字符型没有被建议对齐。

我说“在现代的处理器上”是因为,在一些旧的处理器上,强制让你的C程序违反对齐约束(比方说,将一个奇数的地址转换成一个整型指针,并试图使用它)不仅会使你的代码慢下来,还会造成非法指令的错误。比如在Sun的SPARC芯片上就曾经这么干。实际上,只要够决心并在处理器上设定正确(e18)的硬件标志位,你仍然可以在x86上触发此错误。

此外,自对齐不是唯一的可能的规则。历史上,一些处理器(特别是那些缺少移位暂存器的)有更强的限制性规则。如果你做嵌入式系统,你也许会在跌倒在这些丛林陷阱中。注意,这是有可能的。

有时你可以通过编译指示,强制让你的编译器不使用处理器正常的对齐规则,通常是#pragma pack。不要随意使用,因为它会导致产生开销更大、更慢的代码。使用我在这里描述的技术,通常你可以节省同样或者几乎同样多的内存。

#pragma pack的唯一好处是,如果你不得不将你的C语言数据分布精确匹配到某些位级别的硬件或协议的需求,比如一个内存映射的硬件端口,要求违反正常的对齐才能奏效。如果你遇到那种情况,并且你还未理解我在这里写的这一切,你会有大麻烦的,我只能祝你好运了。

 

 

4. 填充(Padding)

现在我们来看一个简单变量在内存里的分布的例子。考虑在C模块的最顶上的以下一系列的变量声明:

char *p;

char c;

int x;

如果你不知道任何关于数据对齐的事情,你可能会假设这3个变量在内存里会占据一个连续字节空间。那也就是说,在一个32位机器上,指针的4字节,之后紧接着1字节的字符型,且之后紧接着4字节的整型。在64位机器只在指针是8字节上会有所不同。

这里是实际发生的(在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]字符数组表示了一个事实,结构体中有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位机上的长整型

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之后,是保证指针对齐的,肯定比整型对齐更严格的。

M的值不太能预测。如果编译器恰巧把c映射到机器字的最后一个字节,下一个字节(p的第一部分)会成为下一个机器字的第一个字节,并且正常地指针对齐。M为零。

c更可能会被映射到机器字的第一个字节。在那个情况下,M会是以保证p指针对齐而填补的数——在32位机器

首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C语言编程笔记丨如何理解指向指针.. 下一篇C语言编程笔记丨使用C编译器编写s..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目