在 C++ 中优化空间而不是速度

Posted

技术标签:

【中文标题】在 C++ 中优化空间而不是速度【英文标题】:Optimizing for space instead of speed in C++ 【发布时间】:2011-01-06 03:59:37 【问题描述】:

当您说“优化”时,人们往往会想到“速度”。但是对于速度不是那么关键但内存是主要限制因素的嵌入式系统呢?有哪些指南、技术和技巧可用于减少 ROM 和 RAM 中多余的千字节?一个“配置文件”代码如何查看内存膨胀的位置?

附:有人可能会争辩说,“过早地”优化嵌入式系统中的空间并不是那么邪恶,因为您为数据存储和功能蔓延留出了更多空间。它还允许您降低硬件生产成本,因为您的代码可以在更小的 ROM/RAM 上运行。

附言也欢迎参考文章和书籍!

P.P.P.S.这些问题密切相关:404615、1561629

【问题讨论】:

Check this answer. 【参考方案1】:

优化是一个流行的术语,但通常在技术上是不正确的。它的字面意思是优化。无论是速度还是尺寸,这样的条件从未真正实现过。我们可以简单地采取措施进行优化。

许多(但不是全部)用于实现计算结果最短时间的技术会牺牲内存需求,而许多(但不是全部)用于实现最低内存需求的技术会延长得出结果的时间。

减少内存需求相当于使用固定数量的通用技术。很难找到不完全适合其中一个或多个的特定技术。如果您完成了所有这些操作,那么您将拥有非常接近程序最小空间需求的东西,如果不是绝对最小值的话。对于一个真正的应用程序,一个经验丰富的程序员团队可能需要一千年的时间才能完成。

    从存储的数据中删除所有冗余,包括中间数据。 不再需要存储可以流式传输的数据。 只分配需要的字节数,永远不要多一个。 删除所有未使用的数据。 删除所有未使用的变量。 在不再需要数据时立即免费提供数据。 删除所有未使用的算法和算法中的分支。 找出在最小尺寸的执行单元中表示的算法。 删除项目之间所有未使用的空间。

这是该主题的计算机科学观点,而不是开发人员的观点。

例如,打包数据结构是结合上述 (3) 和 (9) 的工作。压缩数据是至少部分实现上述(1)的一种方式。减少高级编程结构的开销是在 (7) 和 (8) 中取得一些进展的一种方法。动态分配是一种尝试利用多任务环境来使用 (3)。编译警告(如果打开)可以帮助解决 (5)。析构函数试图协助 (6)。套接字、流和管道可用于完成 (2)。简化多项式是一种在 (8) 中取得进展的技术。

对九的含义以及实现它们的各种方法的理解是多年学习和检查编译产生的记忆图的结果。由于可用内存有限,嵌入式程序员通常可以更快地学习它们。

在 gnu 编译器上使用 -Os 选项会向编译器发出请求,以尝试找到可以转换的模式以完成这些操作,但 -Os 是一个聚合标志,可以打开许多优化功能,每个它尝试执行转换以完成上述 9 项任务之一。

编译器指令可以在没有程序员努力的情况下产生结果,但编译器中的自动化过程很少能纠正由于代码编写者缺乏意识而产生的问题。

【讨论】:

大小与速度并不是唯一的权衡。另一个因素是可维护性。 没有异议,@DouglasDaseeco【参考方案2】:

在其他人的建议之上:

限制使用 c++ 功能,像在 ANSI C 中一样进行编写,并进行少量扩展。标准 (std::) 模板使用大型动态分配系统。如果可以,请完全避免使用模板。虽然本质上没有害处,但它们使得从几个简单、干净、优雅的高级指令生成大量机器代码变得太容易了。这鼓励了以一种方式编写——尽管有所有“干净代码”的优点——但非常消耗内存。

如果您必须使用模板,请编写您自己的模板或使用为嵌入式使用而设计的模板,将固定大小作为模板参数传递,并编写一个测试程序以便您可以测试您的模板并检查您的 -S 输出以确保编译器不是生成可怕的汇编代码来实例化它。

手动对齐结构,或使用#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.

出于同样的原因,使用集中的全局数据存储结构而不是分散的局部静态变量。

智能平衡 malloc()/new 和静态结构的使用。

如果您需要给定库的功能子集,请考虑自己编写。

展开短循环。

for(i=0;i<3;i++) transform_vector[i]; 

transform_vector[0];
transform_vector[1];
transform_vector[2];

对于较长的不要这样做。

将多个文件打包在一起,让编译器内联短函数并执行链接器无法执行的各种优化。

【讨论】:

链接器这些平台不能。此外,完全禁止模板是无知的,除非您知道自己在做什么,否则我会说没有模板。 您绝对可以使用模板,否则您会使用类似函数的宏。它不应该产生更多的膨胀,并且您可以获得额外的类型安全性。 如果指定-Os,编译器不应该知道何时展开循环以获得更小的空间吗? 如果您小心使用模板,一切都很好。但是,您确定维护代码的人不会被您滥用吗?它们是有风险的,因为它们倾向于使用占用大量内存的编程习惯(在所有其他情况下,这恰好是良好的编码实践——更清洁的源代码)。【参考方案3】:

不要害怕在程序中编写“小语言”。有时,一张字符串表和一个解释器可以完成很多工作。例如,在我工作过的系统中,我们有很多内部表,必须以各种方式访问​​(循环,等等)。我们有一个用于引用表格的内部命令系统,它形成了一种中途语言,对于它所获得的内容来说非常紧凑。

但是,要小心!知道你在写这样的东西(我自己不小心写了一个),并记录你在做什么。最初的开发人员似乎并没有意识到他们在做什么,因此管理起来比应有的要困难得多。

【讨论】:

我同意 Michael 的观点:文档在最终编译的程序中不占用任何空间。使用地段。 我什至不需要很多。总有一天会很好。【参考方案4】:

除了其他人所说的之外,我只想补充一点,不要使用虚函数,因为使用虚函数必须创建一个 VTable,它会占用谁知道有多少空间。

还要注意例外情况。使用 gcc,我不相信每个 try-catch 块的大小都会增加(每个 try-catch 有 2 个函数 calls 除外),但是有一个固定大小的函数必须链接其中可以是浪费宝贵的字节

【讨论】:

类的祖先只有一个 vtable,而不是每个对象(但不确定多重继承)。 vtable 的空间是每个虚拟方法、每个类的一个函数指针。多态对象只拥有一个指向该公共 vtable 的额外指针。恕我直言,vtable + vtable-pointers 并不比使用“类型代码”、switch 语句和调度表的手写替代方案大(除了可能是琐碎的情况)。 关于虚函数,我谦虚地认为更好的指导方针是不要不必要地使用虚函数。仅在需要多态性的地方使用它们。【参考方案5】:

好吧,大部分都已经提到了,但这是我的清单:

了解您的编译器可以做什么。阅读编译器文档,试验代码示例。检查设置。 在目标优化级别检查生成的代码。有时结果令人惊讶,而且通常优化实际上会减慢速度(或者只是占用太多空间)。 选择合适的内存模型。如果您的目标是非常小的紧凑系统,那么大内存模型可能不是最佳选择(但通常最容易编程......) 首选静态分配。仅在启动或结束时使用动态分配 静态分配的缓冲区(池或最大实例大小的静态缓冲区)。 使用 C99 样式数据类型。对于存储类型,使用最小的足够数据类型。对于“快速”数据类型,循环变量等局部变量有时会更有效。 选择内联候选人。内联时,一些具有相对简单主体的参数繁重的函数会更好。或者考虑传递参数的结构。全局变量也是一种选择,但要小心 - 如果其中的任何人没有受到足够的训练,测试和维护会变得很困难。 好好使用 const 关键字,注意数组初始化的含义。 地图文件,最好还有模块大小。还要检查 crt 中包含的内容(真的有必要吗?)。 递归只是说不(有限的堆栈空间) 浮点数 - 更喜欢定点数学。倾向于包含和调用大量代码(即使是简单的加法或乘法)。 C++ 你应该非常了解 C++。如果你不这样做,请用 C 语言编程受限的嵌入式系统。那些敢于使用所有高级 C++ 构造(继承、模板、异常、重载等)的人必须小心。考虑接近硬件代码是 而是在重要的地方使用 Super-C 和 C++:在高级逻辑、GUI 等中。 在编译器设置中禁用您不需要的任何内容(无论是库的一部分、语言结构等)

最后但同样重要的是 - 在寻找尽可能小的代码大小时 - 不要过度。还要注意性能和可维护性。过度优化的代码往往会很快衰减。

【讨论】:

【参考方案6】:

请记住某些 C++ 功能的实现成本,例如虚函数表和创建临时对象的重载运算符。

【讨论】:

【参考方案7】:

这是一本关于该主题的书Small Memory Software: Patterns for systems with limited memory。

【讨论】:

【参考方案8】:

分析代码或数据膨胀可以通过映射文件完成:gcc 见here,VS 见here。 不过,我还没有看到一个有用的尺寸分析工具(而且没有时间修复我的 VS AddIn hack)。

【讨论】:

映射文件还可以帮助解决数据膨胀问题 - 很容易看到您在哪里分配了大块内存,以确定您可能最有效地针对减少工作的目标。 谢谢,应该在里面 - 已添加。【参考方案9】:

几个明显的

如果速度不是关键,直接从闪存执行代码。 使用const 声明常量数据表。这将避免数据从闪存复制到 RAM 使用最小的数据类型紧密地打包大型数据表,并以正确的顺序避免填充。 对大量数据使用压缩(只要压缩代码不超过数据) 关闭异常处理和 RTTI。 有人提到使用-Os 吗? ;-)

将知识折叠成数据

Unix philosophy 的其中一条规则可以帮助代码更紧凑:

表示规则:将知识折叠成数据,这样程序逻辑就可以既愚蠢又健壮。

我已经数不清有多少次我看到复杂的分支逻辑,跨越许多页面,可以折叠成一个漂亮的紧凑表,包含规则、常量和函数指针。状态机通常可以用这种方式表示(状态模式)。命令模式也适用。这完全是关于声明式与命令式的编程风格。

日志代码+二进制数据而不是文本

不是记录纯文本,而是记录事件代码和二进制数据。然后使用“短语手册”来重构事件消息。短语手册中的消息甚至可以包含 printf 样式的格式说明符,以便事件数据值在文本中整齐地显示。

尽量减少线程数

每个线程都需要它自己的内存块用于堆栈和 TSS。如果您不需要抢占,请考虑让您的任务在同一个线程中协同执行 (cooperative multi-tasking)。

使用内存池而不是囤积

为了避免堆碎片,我经常看到单独的模块囤积大量静态内存缓冲区供自己使用,即使只是偶尔需要内存。可以改为使用内存池,因此仅“按需”使用内存。但是,这种方法可能需要仔细分析和检测,以确保池在运行时不会耗尽。

仅在初始化时动态分配

在只有一个应用程序无限期运行的嵌入式系统中,您可以以一种不会导致碎片的合理方式使用动态分配:只需在各种初始化例程中动态分配一次,永远不要释放内存。 reserve() 你的容器到正确的容量,不要让它们自动增长。如果您需要频繁分配/释放数据缓冲区(例如,用于通信数据包),请使用内存池。我什至扩展了 C/C++ 运行时,这样如果在初始化序列之后有任何尝试动态分配内存,它就会中止我的程序。

【讨论】:

"日志代码 + 二进制数据而不是文本" - 我们曾经在二进制文件上运行 strings,按长度对结果进行排序,拍摄图像中最长的字符串,重复直到无聊为止而是去做一些更有趣的事情。那不是 C++,尽管我们确实有要忽略的错位函数名称。【参考方案10】:

如果您正在寻找一种分析应用程序堆使用情况的好方法,请查看 valgrind 的 massif 工具。它可以让您拍摄应用程序的内存使用情况随时间变化的快照,然后您可以使用该信息更好地了解“容易实现的目标”在哪里,并相应地进行优化。

【讨论】:

【参考方案11】:

与所有优化一样,首先优化算法,然后优化代码和数据,最后优化编译器。

我不知道你的程序是做什么的,所以我无法就算法提供建议。许多其他人写过关于编译器的文章。所以,这里有一些关于代码和数据的建议:

消除代码中的冗余。任何三行或更多行的重复代码,在您的代码中重复三次,都应更改为函数调用。 消除数据中的冗余。找到最紧凑的表示:合并只读数据,并考虑使用压缩代码。 通过常规分析器运行代码;删除所有未使用的代码。

【讨论】:

请遵循这个建议 - 我正在开发一个系统,原始开发人员(20 年前)非常关注堆栈,以至于他们到处重复代码!这是一场史诗般的噩梦。【参考方案12】:

从您的链接器生成一个映射文件。它将显示内存是如何分配的。在优化内存使用时,这是一个好的开始。它还将显示所有功能以及代码空间的布局方式。

【讨论】:

【参考方案13】:

在 VS 中使用 /Os 编译。通常这甚至比优化速度还要快,因为更小的代码大小 == 更少的分页。

应该在链接器中启用 Comdat 折叠(默认情况下在发布版本中)

注意数据结构打包;通常这会导致编译器生成更多代码(== 更多内存)来生成程序集以访问未对齐的内存。 Using 1 bit for a boolean flag is a classic example.

另外,在选择内存效率高的算法而不是运行时间更好的算法时要小心。这就是过早优化的用武之地。

【讨论】:

【参考方案14】:

我在一个极度受限的嵌入式内存环境中的经验:

使用固定大小的缓冲区。不要使用指针或动态分配,因为它们的开销太大。 使用最小的 int 数据类型。 永远不要使用递归。始终使用循环。 不要传递大量函数参数。改用全局变量。 :)

【讨论】:

我以为每个人都在根据经验说话……他们还有什么资格?! :D 实际上,如果您考虑一下人们过去如何在内存受限的系统上编程(以及随后的两位数年份问题,但那是另一回事),这是完全有道理的。这种类型的程序架构会小很多。你真的会对人们设法适应非常小的计算机系统感到非常惊讶(回到真正的程序员时代;-)。 全局变量或大量函数参数的一种替代方法是使用参数块。基本上,您创建了一个struct,它可以被多个函数使用,每个函数都使用他们需要的来自 PB 的任何参数。然后调用代码可以设置 PB 并将其传递给一个或多个函数。旧 Mac OS 中的低级文件系统调用从一开始就是这样做的,以帮助将所有内容打包到原始 Macintosh 的 128K 中。它就像 ghetto 类,除了(与类方法不同),您可以将两个 PB 传递给某些函数。 对所有这些都是肯定的,并且:不要(永远)使用浮点数学,确保你的结构紧凑,放弃使用位域,在创建另一个变量之前仔细考虑;如果您可以从现有的信息中获取所需的信息,请执行此操作。 如果你有 256 字节的 RAM 已经保存了 C 堆栈,那么全局变量根本就不是火焰材料。 @Ariel:FP 数学不依赖于实际平台吗?【参考方案15】:

你可以做很多事情来减少你的内存占用,我相信人们已经写了关于这个主题的书,但其中一些主要的是:

减少代码大小的编译器选项(包括 -Os 和打包/对齐选项)

去除死代码的链接器选项

如果您是从闪存(或 ROM)加载到 ram 以执行(而不是从闪存执行),则使用压缩的闪存映像,并使用引导加载程序对其进行解压缩。

使用静态分配:堆是分配有限内存的一种低效方式,如果它受到约束,可能会因碎片而失败。

用于查找堆栈高水位线的工具(通常它们用模式填充堆栈,执行程序,然后查看模式保留的位置),因此您可以优化设置堆栈大小

当然,优化用于内存占用的算法(通常以速度为代价)

【讨论】:

另一方面,堆提供了静态分配所没有的内存重用的可能性。 嗯,堆使得重用内存变得更容易,而无需明确这样做。 关于碎片化角度的正确看法:许多必须运行多年的嵌入式系统拒绝使用动态分配的主要原因。 这样,由于不必处处处理故障,您可以节省大约 30% 的代码大小 ;-) @Emile:在非常有限的环境中,由于严格的限制,您经常不得不打破“良好”的编程习惯。【参考方案16】:

首先,告诉您的编译器针对代码大小进行优化。 GCC 对此有 -Os 标志。

其他一切都在算法级别 - 使用与查找内存泄漏类似的工具,而是寻找可以避免的分配和释放。

还可以看看常用的数据结构打包——如果你能减少一两个字节,你就可以大大减少内存使用。

【讨论】:

以上是关于在 C++ 中优化空间而不是速度的主要内容,如果未能解决你的问题,请参考以下文章

如果我优化大小而不是速度,为什么GCC会生成15-20%的代码?

如何提高交易系统的运行速度:C和C++速度优化

通过将字段存储为字节而不是数十亿文档的字符串,将在 Lucene 索引中优化多少空间和处理

C++优化内存读取速度

杀毒扫描速度优化

优化C++软件