【问题标题】:What are some best practices for reducing memory usage in C?在 C 中减少内存使用的最佳实践是什么?
【发布时间】:2010-09-29 02:39:04
【问题描述】:

什么是“内存高效的 C 编程”的一些最佳实践。 主要针对嵌入式/移动设备,低内存消耗的准则应该是什么?

我想对于 a) 代码内存 b) 数据内存应该有单独的指导方针

【问题讨论】:

  • 很高兴看到这么多完全不同的好答案,而不是重复。
  • 让我完善你的标题。 “内存效率”也可能意味着读取和/或写入速度。

标签: c optimization memory-management


【解决方案1】:

在 C 中,在更简单的层面上,考虑以下内容;

  • 使用 #pragma pack(1) 对结构进行字节对齐
  • 在结构可以包含不同类型数据的情况下使用联合
  • 使用位域而不是整数来存储标志和小整数
  • 避免使用固定长度的字符数组来存储字符串,实现字符串池并使用指针。
  • 在哪里存储对枚举字符串列表的引用,例如字体名称,将索引存储到列表中而不是字符串中
  • 使用动态内存分配时,请提前计算所需的元素数量以避免重新分配。

【讨论】:

  • 啊,我自己完全忘记了前三点。我觉得太明显了,就忽略了。 xD
  • 是的。上面的答案是在阅读了您的 nimrodms 和 monjardins 之后添加的,并且是为了添加所有更明显的遗漏。
  • 1.这可能会带来速度惩罚。更好:智能地构建结构:struct s { int i;字符 c; } 而不是 struct s {char c; int i;} 2. 联合非常好。 3. 位域可能非常缓慢。当心
  • @Mikeage - 是的,我发现通常你要么优化内存,要么优化速度,这两者是对立的。
  • 大部分嵌入式系统都有位操作的指令,所以位域是个不错的选择
【解决方案2】:

我发现在使用嵌入式系统时有用的一些建议:

  • 确保所有查找表或其他常量数据实际上是使用const 声明的。如果使用const,则数据可以存储在只读(例如闪存或 EEPROM)存储器中,否则必须在启动时将数据复制到 RAM,这会占用闪存和 RAM 空间。设置链接器选项,使其生成一个映射文件,并研究此文件以准确了解数据在内存映射中的分配位置。

  • 确保您使用了所有可用的内存区域。例如,微控制器通常具有可供您使用的板载内存(访问速度也可能比外部 RAM 更快)。您应该能够使用编译器和链接器选项设置来控制代码和数据分配到的内存区域。

  • 要减少代码大小,请检查编译器的优化设置。大多数编译器都有开关来优化速度或代码大小。值得尝试使用这些选项来查看是否可以减少编译代码的大小。显然,尽可能消除重复代码。

  • 检查您的系统需要多少堆栈内存并相应地调整链接器内存分配(请参阅this question 的答案)。为了减少堆栈的使用,请避免在堆栈上放置大型数据结构(对于任何与您相关的“大”值)。

【讨论】:

    【解决方案3】:

    确保尽可能使用定点/整数数学。当简单的缩放整数数学就足够时,许多开发人员使用浮点数学(以及缓慢的性能和大型库和内存使用)。

    【讨论】:

    • 这假设系统缺少 FPU,并且需要模拟浮点运算。尽管如此,在大多数情况下,浮点数和双精度数都比定点整数占用更多内存,因此使用后者确实有所帮助。
    • 大多数系统缺少 FPU。整数规则!
    【解决方案4】:

    所有好的建议。以下是一些我发现有用的设计方法。

    • 字节编码

    为特殊用途的字节码指令集编写解释器,并在该指令集中编写尽可能多的程序。如果某些操作需要高性能,请将它们设为本机代码并从解释器中调用它们。

    • 代码生成

    如果部分输入数据很少更改,您可以使用外部代码生成器来创建临时程序。这将比更通用的程序更小,运行速度更快,并且不必为很少更改的输入分配存储空间。

    • 成为数据仇恨者

    如果它能让你存储绝对最小的数据结构,那就愿意浪费很多周期。通常你会发现性能受到的影响很小。

    【讨论】:

    • 在这种情况下,“周期”是什么意思?
    • @Someone:机器周期、指令周期、机器执行的小步骤需要时间。如果您有一台 1 GHz 的机器,则每条指令都需要多个周期,平均可能需要 10 个周期。因此,一个需要 1 亿条指令的程序将需要 10 亿个周期,也就是一秒。这听起来像是一个很大的数字,但在典型的现代软件中,它一点也不大。
    【解决方案5】:

    您很可能需要仔细选择算法。针对具有 O(1) 或 O(log n) 内存使用(即低)的算法。例如,在大多数情况下,连续可调整大小的数组(例如std::vector)比链表需要更少的内存。

    有时,使用查找表可能更有益于代码大小速度。如果 LUT 中只需要 64 个条目,那么与大型 sin/cos/tan 函数相比,sin/cos/tan(使用对称性!)是 16*4 字节。

    压缩有时会有所帮助。像 RLE 这样的简单算法在顺序读取时很容易压缩/解压缩。

    如果您要处理图形或音频,请考虑不同的格式。调色板或 bitpacked* 图形可能是质量的良好折衷,并且调色板可以在许多图像之间共享,从而进一步减少数据大小。音频可能会从 16 位降低到 8 位甚至 4 位,并且可以将立体声转换为单声道。采样率可以从 44.1KHz 降低到 22kHz 或 11kHz。这些音频转换大大减少了它们的数据大小(遗憾的是,质量)并且是微不足道的(除了重新采样,但这就是音频软件的作用=])。

    * 我想你可以把它压缩一下。图形的位打包通常是指减少每个通道的位数,以便每个像素可以容纳两个字节(例如 RGB565 或 ARGB155)或一个(ARGB232 或 RGB332),而不是原来的三个或四个(分别为 RGB888 或 ARGB8888)。

    【讨论】:

      【解决方案6】:

      使用您自己的内存分配器(或小心使用系统的分配器)避免内存碎片。

      一种方法是使用“slab 分配器”(例如,参见 article)和多个内存池用于不同大小的对象。

      【讨论】:

      • +1。当我们需要在 8mb 内存中运行完整的 HTML 4 浏览器时,这是一个成功的因素。
      • 哇……怎么样!?按照现代标准,8mb 太小了!
      【解决方案7】:

      预先分配所有内存(即除了启动初始化之外没有 malloc 调用)对于确定性内存使用肯定有帮助。否则,不同的架构提供了帮助的技术。例如,某些 ARM 处理器提供了一个替代指令集 (Thumb),通过使用 16 位指令而不是普通的 32 位指令,该指令集几乎将代码大小减半。当然,这样做会牺牲速度...

      【讨论】:

      • 对于复杂的算法,ARM 模式可能会减少代码大小。对于简单的操作,THUMB 模式可能会更好。这一切都取决于上下文,真的。但是对于通用代码,我同意 THUMB 比 ARM 小。
      • 预分配适用于某些应用程序的速度和大小。对于其他人来说,它可能会恶化大小要求,因为应用程序通常不能跨模块重复使用内存。
      【解决方案8】:

      可以在字节到达时对流执行一些解析操作,而不是复制到缓冲区并解析。

      这方面的一些例子:

      1. 使用状态机解析 NMEA 流,仅将所需字段收集到更高效的结构中。
      2. 使用 SAX 而不是 DOM 解析 XML。

      【讨论】:

      • +1 用于避免 DOM。甚至在具有大量序列化数据的桌面应用程序上也是如此。
      • 避免文本,使用二进制。 XML 甚至比文本更糟糕。我们的前辈想出 ASN.1 之类的东西是有原因的,它并不易于使用。
      【解决方案9】:

      1) 在您开始项目之前,建立一种测量您正在使用的内存量的方法,最好以每个组件为基础。这样,每次进行更改时,您都可以看到它对内存使用的影响。您无法优化无法衡量的内容。

      2) 如果项目已经成熟并达到内存限制,(或移植到内存较少的设备),请找出您已经使用内存的目的。

      我的经验是,在修复超大应用程序时,几乎所有重要的优化都来自少量更改:减少缓存大小,去除一些纹理(当然这是需要利益相关者同意的功能更改,即会议,因此在您的时间方面可能效率不高),重新采样音频,减少自定义分配堆的前期大小,找到释放仅临时使用的资源并在需要时重新加载它们的方法。偶尔你会发现一些 64 字节的结构可以减少到 16 字节,或者其他什么,但这很少是最容易实现的结果。但是,如果您知道应用程序中最大的列表和数组是什么,那么您就知道首先要查看哪些结构。

      哦,是的:查找并修复内存泄漏。任何可以在不牺牲性能的情况下恢复的记忆都是一个很好的开始。

      过去我花了很多时间 担心代码大小。主要考虑因素(除了:确保您在构建时对其进行测量,以便您可以看到它的变化)是:

      1) 找出引用了哪些代码,以及引用了什么。如果您发现整个 XML 库被链接到您的应用程序只是为了解析一个二元素配置文件,请考虑更改配置文件格式和/或编写您自己的简单解析器。如果可以的话,使用源代码或二进制分析来绘制一个大的依赖图,并寻找只有少数用户的大型组件:可能只需要少量的代码重写就可以将它们剪掉。准备好扮演外交官:如果您的应用程序中有两个不同的组件使用 XML,并且您想要削减它,那么您必须让这两个人相信手动滚动当前受信任的现成库的好处.

      2) 乱用编译器选项。请查阅特定于平台的文档。例如,由于内联,您可能希望减少默认可接受的代码大小增加,而在 GCC 上,您至少可以告诉编译器仅应用通常不会增加代码大小的优化。

      3) 尽可能利用目标平台上已有的库,即使这意味着编写适配器层。在上面的 XML 示例中,您可能会发现在您的目标平台上,内存中始终存在一个 XML 库,因为操作系统使用它,在这种情况下动态链接到它。

      4) 正如其他人所提到的,拇指模式可以帮助 ARM。如果您只将它用于对性能不重要的代码,而将关键的例程留在 ARM 中,那么您将不会注意到差异。

      最后,如果您对设备有足够的控制权,您可能会玩一些巧妙的技巧。 UI 一次只允许一个应用程序运行?卸载您的应用不需要的所有驱动程序和服务。屏幕是双缓冲的,但您的应用程序与刷新周期同步?您也许可以回收整个屏幕缓冲区。

      【讨论】:

        【解决方案10】:
        【解决方案11】:
        • 尽量减少字符串常量的长度并消除尽可能多的字符串常量以减少代码空间

        • 在需要时仔细考虑算法与查找表的权衡

        • 注意不同类型的变量是如何分配的。

          • 常量可能在代码空间中。
          • 静态变量可能位于固定的内存位置。尽可能避免这些。
          • 参数可能存储在堆栈或寄存器中。
          • 局部变量也可以从堆栈中分配。如果代码在最坏的情况下可能会耗尽堆栈空间,请不要声明大型本地数组或字符串。
          • 您可能没有堆 - 可能没有操作系统来为您管理堆。这可以接受吗?您需要 malloc() 函数吗?

        【讨论】:

          【解决方案12】:

          我有一个来自嵌入式系统会议的关于这个主题的演讲。它是从 2001 年开始的,但它仍然非常适用。见paper

          此外,如果您可以选择目标设备的架构,例如使用带有 Thumb V2 的现代 ARM,或带有 VLE 的 PowerPC,或带有 MIPS16 的 MIPS,或者选择已知紧凑的目标,如英飞凌 TriCore 或 SH家庭是一个非常好的选择。更不用说非常紧凑的 NEC V850E 系列了。或者转移到具有出色代码紧凑性的 AVR(但是是 8 位机器)。除了固定长度的 32 位 RISC 之外的任何东西都是不错的选择!

          【讨论】:

            【解决方案13】:

            除了其他人给出的建议外,请记住,在您的函数中声明的局部变量通常会分配在堆栈上。

            如果堆栈内存有限,或者您想减小堆栈大小以便为更多堆或全局 RAM 腾出空间,请考虑以下事项:

            1. 扁平化你的调用树以减少在任何给定时间堆栈上的变量数量。
            2. 将大型局部变量转换为全局变量(减少使用的堆栈数量,但增加使用的全局 RAM 数量)。可以声明变量:

              • 全局范围:对整个程序可见
              • 文件范围内的静态:在同一文件中可见
              • 函数范围内的静态:在函数内可见
              • 注意:无论如何,如果进行了这些更改,如果您有 preemptive 环境,则必须警惕 reentrant 代码的问题。

            许多嵌入式系统没有堆栈监视器诊断以确保捕获stack overflow,因此需要进行一些分析。

            PS:适当使用Stack Overflow的奖励?

            【讨论】:

              【解决方案14】:

              另一个有用的技巧是使用#define 代替那些值不会改变的变量。例如:如果某些东西被定义为 const,则用#define 替换它,因为它们在预处理器阶段被解码并且不占用空间。定义的任何其他数据类型都需要内存空间。这将有助于减少内存使用量。这仅适用于不关心变量类型的情况,因为#define 不会处理它。

              【讨论】:

                【解决方案15】:

                在应用程序中有用的一个技巧是创建一个未雨绸缪的内存基金。在启动时分配足够大的单个块,足以完成清理任务。如果 malloc/new 失败,释放未雨绸缪的资金并发布一条消息,让用户知道资源紧张,他们应该尽快保存。这是 1990 年左右在许多 Mac 应用程序中使用的技术。

                【讨论】:

                  【解决方案16】:

                  限制内存需求的一个好方法是尽可能依赖 libc 或其他可以动态链接的标准库。您必须在项目中包含的每个附加 DLL 或共享对象都是您可以避免烧毁的大量内存。

                  此外,在适用的情况下,使用联合和位字段,仅加载程序在内存中处理的部分数据,并确保使用 -Os(在 gcc 中;或您的编译器的等效项)进行编译) 切换以优化程序大小。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-10-31
                    • 2019-01-30
                    • 2018-11-04
                    • 1970-01-01
                    • 2018-03-13
                    • 1970-01-01
                    相关资源
                    最近更新 更多