在C ++中优化空间而不是速度
当你说“优化”时,人们倾向于认为“速度”。 但是embedded式系统的速度并不是那么重要,但是内存是一个主要的限制因素呢? 什么是一些指导方针,技巧和技巧,可用于削减ROM和RAM中的这些额外的千字节? 一个“configuration文件”代码如何看到内存膨胀在哪里?
PS有人可能会争辩说,在embedded式系统中过早地优化空间并不是那么糟糕,因为你为数据存储和蠕变提供了更多的空间。 它还允许您降低硬件生产成本,因为您的代码可以在较小的ROM / RAM上运行。
PPS也欢迎参考文章和书籍!
PPPS这些问题密切相关: 404615,1561629
我从一个非常有限的embedded式内存环境的经验:
- 使用固定大小的缓冲区 不要使用指针或dynamic分配,因为它们有太多的开销。
- 使用最小的int数据types。
- 永远不要使用recursion。 始终使用循环。
- 不要传递大量的函数参数。 改用全局variables。 🙂
你可以做许多事情来减less你的记忆,我相信人们已经写了关于这个主题的书籍,但其中一些主要的是:
-
编译器选项以减less代码大小(包括-Os和包装/alignment选项)
-
链接器选项剥离死代码
-
如果您正在从闪存(或ROM)加载到RAM(而不是从闪存执行),请使用压缩的闪存映像,然后使用引导加载程序对其进行解压缩。
-
使用静态分配:堆是分配有限内存的一种低效率的方法,并且如果它受到限制,可能由于碎片而失败。
-
find堆栈高水位的工具(通常他们用一个模式填充堆栈,执行程序,然后看模式在哪里),所以你可以优化地设置堆栈的大小
-
当然,优化用于内存占用的algorithm(通常以速度为代价)
一些明显的
- 如果速度不重要,请直接从闪存执行代码。
- 使用
const
声明常量数据表。 这将避免数据从闪存复制到RAM - 使用最小的数据types紧紧包装大数据表,并按正确顺序避免填充。
- 对大量数据使用压缩(只要压缩代码不超过数据)
- closuresexception处理和RTTI。
- 有人提到使用-Os? 😉
将知识折叠成数据
Unix哲学的一个规则可以帮助代码更紧凑:
表示规则:将知识折叠成数据,所以程序逻辑可以是愚蠢的和强大的。
我不能计算多less次我已经看到精心制作的分支逻辑,跨越多个页面,可以被折叠成一个很好的规则,常量和函数指针的紧凑表。 状态机通常可以用这种方式表示(状态模式)。 命令模式也适用。 这完全是关于编程的声明式和命令式的。
日志代码+二进制数据,而不是文本
而不是logging纯文本,日志事件代码和二进制数据。 然后使用“短语”重构事件消息。 短语集中的消息甚至可以包含printf样式的格式说明符,以便事件数据值在文本中整齐地显示。
尽量减less线程数量
每个线程都需要它自己的内存块,用于堆栈和TSS。 在不需要抢占的地方,考虑在同一个线程内合作执行任务( 协作多任务 )。
使用内存池而不是囤积
为了避免堆碎片,我经常看到单独的模块囤积大量的静态内存缓冲区供自己使用,即使只是偶尔需要内存。 内存池可以用来代替,所以内存只能“按需”使用。 但是,这种方法可能需要仔细分析和检测,以确保池在运行时不会耗尽。
dynamic分配只在初始化
在只有一个应用程序无限期运行的embedded式系统中,您可以以一种合理的方式使用不会导致碎片的dynamic分配:只需在各种初始化例程中dynamic分配一次,并且永不释放内存。 reserve()
您的容器到正确的容量,不要让它们自动增长。 如果您需要频繁分配/释放数据缓冲区(例如通信数据包),请使用内存池。 我曾经甚至扩展了C / C ++运行时,以便在初始化序列之后尝试dynamic分配内存时,它会中止我的程序。
从链接器生成一个映射文件。 它将显示如何分配内存。 优化内存使用情况时,这是一个好的开始。 它还将显示所有function以及代码空间的布局。
与所有优化一样,首先优化algorithm,然后优化代码和数据,最后优化编译器。
我不知道你的程序做什么,所以我不能就algorithm提供build议。 许多人已经写了关于编译器。 所以,这里有一些关于代码和数据的build议:
- 消除代码中的冗余。 在你的代码中重复三次的三行或三行以上的重复代码,应该改为函数调用。
- 消除数据冗余。 find最紧凑的表示法:合并只读数据,并考虑使用压缩代码。
- 通过普通的分析器运行代码; 消除所有未使用的代码。
这里有一本关于小内存软件主题的书:内存有限的系统模式 。
用VS编译VS。 通常情况下,这比速度优化还要快,因为更小的代码尺寸==更less的分页。
应该在链接器中启用Comdat折叠(默认情况下,在发布版本中)
小心数据结构打包; 通常这会导致编译器生成更多的代码(==更多的内存)来生成程序集来访问未alignment的内存。 使用1位布尔标志是一个典型的例子。
另外,在运行时更好的algorithm上select高效的内存algorithm时要小心。 这是过早优化的地方。
好吧,大部分已经提到,但这里是我的列表无论如何:
- 了解你的编译器可以做什么。 阅读编译器文档,试验代码示例。 检查设置。
- 在目标优化级别检查生成的代码 。 有时结果是令人惊讶的,通常情况下优化实际上会减慢速度(或者占用太多的空间)。
- select合适的内存模型 。 如果你的目标是非常小的系统,那么大或者大的内存模型可能不是最好的select(但是通常最容易编程)
- 倾向静态分配 。 仅在启动时或在静态分配的缓冲区(池或最大实例大小的静态缓冲区)上使用dynamic分配。
- 使用C99样式数据types 。 对于存储types,使用最小的充足数据types。 像循环variables这样的局部variables对于“快速”数据types有时更高效。
- select内联候选人。 一些参数比较简单的重function在内联时效果更好。 或考虑传递参数的结构。 全局也是选项,但要小心 – 如果其中任何人没有足够的训练,testing和维护可能会变得困难。
- 使用const关键字,注意数组初始化的含义。
- 映射文件 ,理想情况下也与模块大小。 请检查crt中包含的内容(是否真的需要?)。
- recursion只是说没有(有限的堆栈空间)
- 浮点数 – 喜欢定点math。 趋于包括和调用大量的代码(即使是简单的加法或乘法)。
- C ++你应该知道C ++非常好。 如果你不这样做,请用C语言编写受约束的embedded式系统。 那些敢于使用所有高级C ++构造(inheritance,模板,exception,重载等)的人必须小心。 考虑接近硬件代码而不是Super-C,C ++用于计数:高级逻辑,GUI等。
- 禁用编译器设置中不需要的任何东西(无论是库,语言结构等)
最后但并非最不重要的一点是,尽可能less的代码大小, 不要过分 。 还要注意性能和可维护性。 过度优化的代码很快就会衰减。
首先,告诉编译器对代码大小进行优化。 海湾合作委员会有这个-Os
标志。
其他一切都在algorithm级别 – 使用类似的工具,你会发现内存泄漏,而是寻找可以避免的分配和释放。
也看看常用的数据结构打包 – 如果你能刮掉一两个字节,你可以大大减less内存使用。
如果您正在寻找一种分析应用程序堆使用情况的好方法,请查看valgrind的massif工具。 它可以让你随着时间的推移对应用的内存使用情况进行快照,然后你可以使用这些信息来更好地看到“低挂果子”的位置,并相应地优化你的优化。
剖析代码或数据膨胀可以通过映射文件完成:对于gcc看到这里 ,VS看到这里 。
我还没有看到一个有用的工具,虽然大小分析(并没有时间来解决我的VS AddIn hack)。
在别人的build议之上:
限制使用c ++特性,像在ANSI C中写入一样扩展。 标准(std :)模板使用一个大型的dynamic分配系统。 如果可以的话,完全避免使用模板。 虽然本质上不是有害的,但是它们使得从一些简单,干净,优雅的高级指令中生成很多很多机器代码变得非常容易。 这鼓励写作的方式 – 尽pipe所有“干净的代码”的优势 – 是非常内存饥饿。
如果您必须使用模板,请自行编写或使用专用于embedded式应用的模板,传递固定大小作为模板参数,并编写testing程序,以便testing模板并检查-S输出以确保编译器不会产生可怕的组件代码来实例化它。
用手alignment你的结构,或使用#pragma pack
{char a; long b; char c; long d; char e; char f; } //is 18 bytes, {char a; char c; char d; char f; long b; long d; } //is 12 bytes.
出于同样的原因,使用集中的全局数据存储结构,而不是分散的本地静态variables。
智能地平衡malloc()/ new和static结构的使用。
如果您需要给定库的function子集,请考虑编写自己的库。
展开短循环。
for(i=0;i<3;i++){ transform_vector[i]; }
比…更长
transform_vector[0]; transform_vector[1]; transform_vector[2];
不要那样做更长的。
将多个文件打包在一起,让编译器内联短的函数并执行各种优化。链接器不能。
不要害怕在程序中写“小语言”。 有时候一个string表和一个解释器可以完成很多事情。 例如,在一个我工作的系统中,我们有很多内部表,必须以各种方式访问(无论循环)。 我们已经有了一个内部的引用表的命令系统,这些命令形成了一种中间语言,对于它的内容来说是非常紧凑的。
不过要小心! 知道你正在写这样的东西(我自己写了一个不小心),并logging你在做什么。 原来的开发者似乎并没有意识到自己在做什么,所以pipe理起来要比应该的难得多。
优化是一个stream行的术语,但通常在技术上是不正确的。 它的字面意思是使最佳。 无论是速度还是规模,这样的条件都不可能实现。 我们可以简单地采取措施走向优化。
许多(但不是全部)技术用于计算结果的最小时间牺牲了内存要求,许多(但不是全部)用于最小内存需求的技术延长了结果的时间。
内存需求的减less相当于固定数量的一般技术。 很难find一种不能很好地融入其中一种或多种的特定技术。 如果你做了所有这些,如果不是绝对的最小可能的话,你会有一些非常接近程序的最小空间要求的东西。 对于一个真正的应用程序,可能需要一个有经验的程序员团队一千年才能完成。
- 从存储的数据中删除所有冗余,包括中间体。
- 删除所有需要存储可以stream式传输的数据。
- 只分配所需的字节数,从来没有一个更多。
- 删除所有未使用的数据。
- 删除所有未使用的variables。
- 免费数据一旦不再需要。
- 删除algorithm中所有未使用的algorithm和分支。
- find在最小执行单元中表示的algorithm。
- 删除项目之间的所有未使用的空间。
这是关于这个话题的计算机科学观点,而不是开发者的观点。
例如,打包一个数据结构是一个结合上面(3)和(9)的努力。 压缩数据是至less部分实现上述(1)的一种方式。 (7)和(8)中取得一些进展的方法是降低高层次编程结构的开销。 dynamic分配是尝试利用多任务环境来使用(3)。 编译警告,如果打开,可以帮助(5)。 破坏者试图协助(6)。 套接字,stream和pipe道可以用来完成(2)。 (8)中简化多项式是一种技术。
理解九的含义以及实现它们的各种方法是多年来通过编译所得到的存储器映射的学习和检查的结果。 embedded式程序员经常因为内存有限而更快地学习它们。
在gnu编译器上使用-Os选项向编译器发出请求,试图find可以转换的模式来完成这些模式,但-Os是一个聚合标志,打开了许多优化特性,每个优化特性都尝试执行转换来完成上述9个任务之一。
编译器指令可以产生结果而无需程序员的努力,但是编译器中的自动化过程很less能够纠正由于代码编写者缺乏意识而产生的问题。
请记住一些C ++function的实现成本,例如创build临时对象的虚函数表和重载操作符。
与其他人一样,我只想添加不使用虚拟function,因为虚拟function必须创build一个可以接受谁知道多less空间的VTable。
也要注意例外情况。 对于gcc,我不相信每个try-catch块都有一个增长的大小(除了每个try-catch的2个函数call
)外,还有一个固定大小的函数,它必须链接在其中,可以浪费宝贵字节