代码之家  ›  专栏  ›  技术社区  ›  Emile Cormier

C++中空间代替速度的优化

  •  42
  • Emile Cormier  · 技术社区  · 15 年前

    当你说“优化”时,人们倾向于认为“速度”。但是,如果嵌入式系统的速度不是那么关键,但是内存是一个主要限制,那该怎么办呢?哪些指导方针、技术和技巧可以用来减少ROM和RAM中多余的千字节?一个“概要”代码如何查看内存膨胀的位置?

    P.S.有人可能会说,在嵌入式系统中“过早地”优化空间并不是那么糟糕,因为您为数据存储和特性爬行留出了更多的空间。它还允许您降低硬件生产成本,因为您的代码可以在较小的ROM/RAM上运行。

    P.P.S.也欢迎参考文章和书籍!

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

    16 回复  |  直到 8 年前
        1
  •  30
  •   Tim Lovell-Smith    15 年前

    我的经验来自 极其 受限的嵌入式内存环境:

    • 使用固定大小的缓冲区。不要使用指针或动态分配,因为它们有太多的开销。
    • 使用最小的有效int数据类型。
    • 不要使用递归。始终使用循环。
    • 不要传递很多函数参数。改为使用全局变量。:)
        2
  •  13
  •   James    15 年前

    你可以做很多事情来减少你的记忆足迹,我相信人们都写过关于这个主题的书,但其中一些主要的是:

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

    • 去除死码的链接器选项

    • 如果要从闪存(或ROM)加载到RAM以执行(而不是从闪存执行),请使用压缩的闪存映像,然后使用引导加载程序将其解压缩。

    • 使用静态分配:堆是分配有限内存的一种效率低下的方法,如果受到限制,它可能会由于碎片而失败。

    • 查找堆栈高水位线的工具(通常它们用模式填充堆栈,执行程序,然后查看模式的剩余位置),这样您就可以以最佳方式设置堆栈大小。

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

        3
  •  11
  •   Emile Cormier    15 年前

    一些明显的

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

    将知识折叠成数据

    规则之一 Unix philosophy 有助于使代码更紧凑:

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

    我不知道我见过多少次复杂的分支逻辑,跨越许多页,可以折叠成一个规则、常量和函数指针的紧凑表。状态机通常可以这样表示(状态模式)。命令模式也适用。这都是关于声明式和命令式编程风格的。

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

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

    最小化线程数

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

    使用内存池而不是囤积

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

    仅在初始化时进行动态分配

    在只有一个应用程序无限期运行的嵌入式系统中,您可以以一种不会导致碎片化的合理方式使用动态分配:只在各种初始化例程中动态分配一次,而不释放内存。 reserve() 您的容器容量正确,不允许自动增长。如果您需要频繁地分配/释放数据缓冲区(例如,对于通信数据包),那么使用内存池。我曾经甚至扩展了C/C++运行时,如果在初始化序列之后有任何东西试图动态分配内存,它会中止我的程序。

        4
  •  7
  •   Thomas Matthews    15 年前

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

        5
  •  7
  •   Chip Uni    15 年前

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

    我不知道你的程序是做什么的,所以我不能对算法提出建议。还有很多人写过关于编译器的文章。下面是一些关于代码和数据的建议:

    • 消除代码中的冗余。任何三行或更多行长的重复代码,在代码中重复三次,都应该更改为函数调用。
    • 消除数据中的冗余。查找最紧凑的表示:合并只读数据,并考虑使用压缩代码。
    • 通过常规分析器运行代码;消除所有未使用的代码。
        6
  •  5
  •   Nikolai Fetissov    15 年前
        7
  •  4
  •   Terry Mahaffey    15 年前

    用vs/os编译。通常情况下,这甚至比优化速度更快,因为更小的代码大小==更少的分页。

    应该在链接器中启用COMDAT折叠(在发布版本中是默认的)

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

    另外,在选择一个内存高效的算法而不是运行时间更好的算法时要小心。这就是过早的优化出现的地方。

        8
  •  3
  •   MaR    15 年前

    好吧,大多数人都已经提到过了,但这是我的清单:

    • 了解编译器可以做什么。 阅读编译器文档,用代码示例进行实验。检查设置。
    • 检查生成的代码 在目标优化级别。有时结果会令人惊讶,通常情况下,优化会减慢速度(或者只是占用了太多空间)。
    • 选择合适 记忆模型 .如果您的目标是非常小而紧凑的系统,那么大型或大型内存模型可能不是最佳选择(但通常最容易为……编程)。
    • 喜欢 静态分配 . 仅在启动或重新启动时使用动态分配 静态分配的缓冲区(池或最大实例大小的静态缓冲区)。
    • 使用 C99样式数据类型 . 对存储类型使用最小的足够数据类型。对于“快速”数据类型,局部变量(如循环变量)有时效率更高。
    • 选择 内联的 候选人。一些具有相对简单体的参数重函数在内联时效果更好。或者考虑传递参数的结构。环球也可以选择,但要小心——如果他们中的任何人没有足够的纪律,测试和维护都会变得困难。
    • 使用 康斯特 关键字,请注意数组初始化的含义。
    • 地图文件 理想情况下,也适用于模块尺寸。同时检查CRT中包含的内容(是否确实需要?).
    • 递归 只说不(有限的堆栈空间)
    • 浮点 数字-更喜欢定点数学。倾向于包含和调用大量代码(即使是简单的加法或乘法)。
    • C++ 你应该对C++有很好的了解。如果没有,请在C语言中使用程序约束的嵌入式系统。敢于做的人必须小心所有高级C++结构(继承、模板、异常、重载等)。考虑接近硬件代码 Suff-C和C++被用于计算:高级逻辑、GUI等。
    • 禁用编译器设置中不需要的任何内容(无论是库的一部分、语言结构等)。

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

        9
  •  2
  •   Anon.    15 年前

    首先,告诉编译器优化代码大小。海湾合作委员会有 -Os 这是旗帜。

    其他的一切都是在算法层面上的——使用类似的工具来查找内存泄漏,而是寻找可以避免的分配和释放。

    还可以看看常用的数据结构打包——如果您可以将它们切掉一两个字节,就可以大大减少内存使用。

        10
  •  2
  •   Jeremy Friesner    15 年前

    如果您正在寻找一种分析应用程序堆使用情况的好方法,请查看valgrind的 massif 工具。它可以让你在一段时间内对你的应用程序的内存使用情况进行快照,然后你可以利用这些信息更好地了解“低挂水果”的位置,并相应地瞄准你的优化。

        11
  •  2
  •   Community CDub    8 年前

    分析代码或数据膨胀可以通过映射文件完成:有关gcc,请参见 here ,对于VS here .
    不过,我还没有看到一个有用的大小分析工具(也没有时间修复我的vs-addin hack)。

        12
  •  2
  •   SF.    8 年前

    最重要的是其他人的建议:

    限制使用C++的特性,在ANSIC中写得很小。标准(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()/新结构和静态结构的使用。

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

    展开短循环。

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

    长于

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

    长的不要这样做。

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

        13
  •  1
  •   Michael Kohne    15 年前

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

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

        14
  •  1
  •   Douglas Daseeco    8 年前

    优化是一个流行的术语,但在技术上常常是错误的。它的字面意思是使最佳。无论是速度还是尺寸,这种情况实际上都无法实现。我们可以简单地采取措施走向优化。

    许多(但不是全部)用于向最小时间移动到计算结果的技术牺牲了内存需求,而许多(但不是全部)用于向最小内存需求移动的技术延长了结果的时间。

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

    1. 从存储的数据中删除所有冗余,包括中间数据。
    2. 不再需要存储可以流式处理的数据。
    3. 只分配所需的字节数,不要再分配一个。
    4. 删除所有未使用的数据。
    5. 删除所有未使用的变量。
    6. 一旦不再需要,就立即释放数据。
    7. 删除算法中所有未使用的算法和分支。
    8. 找到最小执行单元中表示的算法。
    9. 删除项目之间所有未使用的空间。

    这是一个关于这个主题的计算机科学视图,而不是开发人员的视图。

    例如,打包数据结构是将上述(3)和(9)结合起来的工作。压缩数据是至少部分实现上述(1)的一种方法。减少更高级别编程结构的开销是实现(7)和(8)中某些进展的一种方法。动态分配是一种利用多任务环境的尝试(3)。编译警告,如果打开,可以帮助(5)。破坏者试图协助(6)。插座、溪流和管道可用于完成(2)。简化多项式是在(8)中获得基础的一种技术。

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

    在GNU编译器上使用-os选项向编译器发出请求,试图找到可以转换的模式来完成这些任务,但是-os是一个聚合标志,它打开许多优化功能,每个功能都试图执行转换来完成上面9个任务中的一个。

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

        15
  •  0
  •   PhilMY    15 年前

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

        16
  •  0
  •   Earlz    15 年前

    除此之外,其他人都说,我只是想补充一点,不要使用虚拟功能,因为对于虚拟功能,必须创建一个vtable,它可以占用谁知道有多少空间。

    还要注意例外。对于gcc,我不认为每个try-catch块的大小都在增加(除了2个函数) call 但是有一个固定大小的函数必须链接,在这个函数中可能会浪费宝贵的字节。