什么是减less内存使用C的一些最佳做法?
“Memory Efficient C programming”的最佳实践是什么? 主要是对于embedded式/移动设备,应该是低内存消耗的准则?
我想应该有单独的指导方针a)代码存储器b)数据存储器
在C中,在一个更简单的水平,考虑以下;
- 使用#pragma pack(1)来alignment你的结构
- 在结构可以包含不同types的数据的情况下使用联合
- 使用位域而不是整数来存储标志和小整数
- 避免使用固定长度的字符数组来存储string,实现一个string池并使用指针。
- 在存储对枚举string列表(例如字体名称)的引用时,将索引存储到列表中而不是string中
- 在使用dynamic内存分配时,请提前计算所需的元素数量以避免重新分配。
我发现了一些对embedded式系统有用的build议:
-
确保使用
const
实际声明任何查找表或其他常量数据。 如果使用const
,则数据可以存储在只读(例如,闪存或EEPROM)存储器中,否则数据必须在启动时被复制到RAM中,这会占用闪存和RAM空间。 设置链接器选项,以便它生成一个映射文件,并研究这个文件,以确切地看到你的数据分配在内存映射的位置。 -
确保你正在使用所有可用的内存区域。 例如,微控制器通常具有可以使用的板上存储器(其也可以比外部RAM更快地访问)。 您应该能够使用编译器和链接器选项设置来控制分配了代码和数据的内存区域。
-
要减less代码大小,请检查编译器的优化设置。 大多数编译器都有开关来优化速度或代码大小。 可以尝试使用这些选项来查看是否可以减less编译代码的大小。 显然,尽可能消除重复的代码。
-
检查您的系统需要多less堆栈内存并相应地调整链接器内存分配(请参阅此问题的答案)。 为了减less堆栈使用,避免将大型数据结构放在堆栈上(无论“大”的值与您有什么关系)。
确保你使用的是定点/整数math。 当简单的缩放整数math就足够了的时候,很多开发人员使用浮点math(以及缓慢的性能和大的库和内存使用)。
所有的好build议。 以下是我发现有用的一些devise方法。
- 字节编码
写一个专用字节码指令集的解释器,并尽可能多地在该指令集中写入程序。 如果某些操作需要高性能,请将其编译为本机代码,并从解释器调用它们。
- 代码生成
如果input数据的一部分变化很less,则可以使用外部代码生成器来创build临时程序。 这比一个更普通的程序要小,而且运行速度更快,不需要为难以改变的input分配存储空间。
- 成为一个数据憎恨者
如果能够让你存储绝对最小的数据结构,那就愿意浪费大量的周期。 通常你会发现性能受损很小。
通过使用自己的内存分配器(或小心使用系统的分配器)来避免内存碎片。
一种方法是使用“slab分配器”(例如参见本文 )和不同大小的对象的多个内存池。
预先分配所有内存(即除启动初始化外不需要malloc调用)肯定有助于确定性的内存使用情况。 否则,不同的体系结构会提供帮助的技术。 例如,某些ARM处理器提供了一个替代指令集(Thumb),通过使用16位指令而不是普通的32位来缩小代码大小。 当然,速度牺牲了这样做…
很可能你需要仔细select你的algorithm。 针对具有O(1)或O(log n)内存使用率(即低)的algorithm。 例如,连续resize的数组(例如std::vector
)在大多数情况下所需的内存less于链接列表。
有时,使用查找表可能更有利于代码大小和速度。 如果您只需要一个LUT中的64个条目,那么与sin / cos / tan函数相比,sin / cos / tan(使用对称!)就是16 * 4个字节。
压缩有时帮助。 RLE等简单algorithm在顺序读取时易于压缩/解压缩。
如果您正在处理graphics或audio,请考虑不同的格式。 调色板或者比特包*graphics可能是一个很好的质量折衷scheme,可以在许多图像上共享调色板,从而进一步缩小数据大小。 audio可能从16位降低到8位甚至4位,立体声可以转换为单声道。 采样率可以从44.1KHz降低到22kHz或11kHz。 这些audio转换大大减less了他们的数据大小(可悲的是,质量),并且是微不足道的(除了重采样,但audio软件是这样的=)。
*我想你可以把它压缩。 graphics的Bitpacking通常是指减less每个通道的位数,使得每个像素可以适应两个字节(例如RGB565或ARGB155)或者一个(ARGB232或RGB332)从原来的三个或四个(分别为RGB888或ARGB8888)。
一些parsing操作可以在字节到达时在stream上执行,而不是复制到缓冲区和parsing。
这样的一些例子:
- 用一台状态机parsing一个NMEA蒸汽,只将必要的字段收集到一个效率更高的结构中。
- 使用SAX而不是DOMparsingXML。
1)在开始项目之前,build立一个测量你正在使用多less内存的方法,最好在每个组件的基础上。 这样,每次你做出改变,你都可以看到它对内存使用的影响。 你无法优化你无法衡量的东西。
2)如果项目已经成熟并达到内存限制(或者移植到内存较less的设备上),请查看你已经使用的内存。
我的经验是,在修复超大尺寸应用程序时,几乎所有的重要优化都来自less量的变化:减lesscaching大小,去掉一些纹理(当然这是一个需要利益相关者协议的function变化,即会议,所以可能在时间上没有效率),重新采样audio,减less自定义分配堆的前期大小,find方法来释放仅临时使用的资源,并在需要时重新加载它们。 偶尔你会发现一些64字节的结构,可以减less到16,或者其他的,但是这个结果很less是最低的。 如果你知道应用程序中最大的列表和数组是什么,那么你知道首先要看哪个结构。
哦,是的:find并修复内存泄漏。 任何能够在不牺牲性能的情况下重获记忆,都是一个很好的开始。
过去我花了很多时间担心代码大小。 主要考虑因素(除了:确保在构build时测量它,以便您可以看到它发生变化):
1)找出哪些代码被引用,并通过什么。 如果您发现整个XML库被链接到您的应用程序只是为了parsing一个双元素configuration文件,请考虑更改configuration文件格式和/或编写自己的平凡parsing器。 如果可以的话,可以使用源代码或二进制分析来绘制一个大的依赖关系图,并查找只有less量用户的大型组件:只需稍微重写一些代码就可以将其分解出来。 准备好扮演外交官:如果你的应用程序中有两个不同的组件需要使用XML,而你想要切断它,那么这两个人就必须说服手动滚动某些目前可信任的,现成的库。
2)编译选项。 请查阅您的平台特定的文档。 例如,你可能希望减less由于内联而导致的默认可接受的代码大小增加,并且在GCC上,至less你可以告诉编译器只应用通常不增加代码大小的优化。
3)尽可能利用目标平台上已有的库,即使这意味着要编写一个适配器层。 在上面的XML示例中,您可能会发现,在目标平台上,无论如何都会在内存中存在一个XML库,因为操作系统使用它,在这种情况下会dynamic链接到它。
4)正如别人提到的,拇指模式可以帮助ARM。 如果您只将它用于不是性能至关重要的代码,并将关键例程留在ARM中,那么您将不会注意到其中的差异。
最后,如果您对设备有足够的控制权,则可以使用巧妙的技巧。 UI只允许一个应用程序一次运行? 卸载您的应用程序不需要的所有驱动程序和服务。 屏幕是双缓冲,但你的应用程序是同步到刷新周期? 您可能能够回收整个屏幕缓冲区。
推荐本书Small Memory Software:内存有限的系统的模式
-
尽量减lessstring常量的长度并尽可能减less代码空间
-
在需要的地方仔细考虑algorithm与查找表的权衡
-
注意如何分配不同types的variables。
- 常量可能在代码空间中。
- 静态variables可能在固定的内存位置。 如果可能的话避免这些。
- 参数可能存储在堆栈或寄存器中。
- 局部variables也可以从堆栈中分配。 如果代码在最坏的情况下可能会用完栈空间,则不要声明大的本地数组或string。
- 你可能没有堆 – 可能没有一个操作系统来为你pipe理堆。 这可以接受吗? 你需要一个malloc()函数吗?
在应用程序中有用的一个技巧是创build一个雨天的记忆基金。 在启动时分配一个足够大的单个块,足以完成清理任务。 如果malloc / new失败,释放雨天基金并发布消息,让用户知道资源紧张,他们应该保存并且很快。 这是1990年以前在许多Mac应用程序中使用的技术。
限制内存需求的一个好方法是尽可能地依靠libc或其他可以dynamic链接的标准库。 每个额外的DLL或共享对象,你必须包括在你的项目是一个重要的内存块,你可能能够避免燃烧。
另外,在适用的情况下,使用联合和位域只加载程序在内存中处理的部分数据,并确保使用-Os编译(在gcc中;或编译器的等价物)切换到优化程序大小。
我有关于这个主题的embedded式系统会议的介绍。 这是从2001年,但仍然是非常适用的。 看到纸 。
此外,如果您可以select目标设备的体系结构,那么使用Thumb V2,PowerPC与VLE或MIPS与MIPS16等现代ARM,或者selectInfineon TriCore或SH系列等已知紧凑型目标是一种非常好的select。 更不用说紧凑的NEC V850E系列了。 或转移到具有优秀的代码紧凑性(但是是一个8位机器)的AVR。 除了固定长度的32位RISC以外,任何事情都是不错的select!
除了其他的build议之外,请记住函数中声明的局部variables通常会分配在堆栈上。
如果堆栈内存有限,或者您希望减小堆栈的大小以便为更多的堆或全局RAM腾出空间,请考虑以下事项:
- 平整您的调用树,以减less堆栈中variables的数量。
-
将大的本地variables转换为全局variables (减less使用的堆栈数量,但增加了全局RAM的使用量)。 variables可以被声明:
- 全球范围:可见整个计划
- 静态文件范围:在同一个文件中可见
- 函数范围内的静态:在函数内可见
- 注意:无论如何,如果进行了这些更改,如果您拥有
preemptive
环境,则必须警惕reentrant
代码的问题。
许多embedded式系统没有堆栈监视器诊断来确保捕获堆栈溢出 ,因此需要进行一些分析。
PS:奖金适当使用堆栈溢出 ?