博客 阅读时间 5 分钟

优化内存使用:来自 Doxygen 的启示

分享本文
Optimizing Memory Usage: Insights from Doxygen

当您机器上运行的进程试图分配超出系统可用容量的内存时,内核就会开始在磁盘之间换入换出内存页。这样做是为了释放足够的物理内存,以满足请求方的内存分配需求。

过度使用交换被称为颠簸(thrashing),这是我们不希望看到的,因为它会降低系统的整体性能——主要原因是硬盘比 RAM 慢得多。

如果您的应用程序需要处理大量数据,它就可能遭遇颠簸问题,速度可能会急剧下降。解决方案有两种:优化应用程序以更有效地使用内存,或者为系统增加更多物理内存。

让我们来看看Doxygen采用了哪种方案来优化内存使用、避免颠簸。

Doxygen 是从带注释的 C++ 源代码生成文档的事实标准工具,但它也支持其他流行的编程语言,如 C、Objective-C、C#、PHP、Java、Python 等。非常感谢 Dimitri van Heesch 为开发和维护该项目付出的巨大努力。

Doxygen 以源文件为输入,解析它们以提取所需数据,并将结果存储在 DirDef、FileDef、NamespaceDef、ClassDef 和 MemberDef 类的实例中。它们都继承自 Definition 类。

doxy7

这些类的实例随后会被用来生成文档。消耗内存最多的数据是关于方法和变量的信息,它们由 MemberDef 类表示。这些实例的大小可能会增长到超过 1 GB,具体取决于所处理项目中方法和变量的数量。

对于某些项目,将所有这些实例存储在内存中会影响系统性能,生成文档可能需要好几个小时。

Doxygen 是如何优化内存的?

Doxygen 使用基于缓存的解决方案。使用缓存(cache)是优化内存使用的常见方式。其思路是将需要驻留内存的数据存储在缓存中。这个缓存包含许多槽位,每个槽位保存一份特定的数据。当缓存超过一定大小时,一些槽位会被释放。被释放的数据会移动到磁盘上,并在再次需要时加载回内存。

在 Doxygen 中,算法非常简单:

  • 定义一个拥有 65535 个槽位的缓存。
  • 当需要创建一个 MemberDef 实例时,Doxygen 检查缓存槽位是否可用。如果可用,就在内存中创建实例;否则,将它存储在磁盘上的数据文件中,并更新一个索引文件,记录这份数据在数据文件中的存储位置。
  • 当 Doxygen 需要访问某个 MemberDef 实例时,它会检查该实例是否存在于缓存中。如果不存在,Doxygen 就利用索引文件确定数据的存储位置,在数据文件中定位到该位置,并从磁盘加载它。

缓存的性能取决于:

  • 容器:可以是队列、数组、链表,或者自定义容器。容器的选择会影响您的缓存性能。
  • 缓存的最大容量。
  • 从缓存中移除条目的算法。当缓存达到最大容量时,您必须决定释放哪些条目。例如,您可以:
    • 释放最先加载的槽位。
    • 释放最后加载的槽位。
    • 释放使用最少的槽位。

1. 容器

Doxygen 定义了 ObjCache 类,它是一个由 CacheNode 对象组成的链表。这个类负责向缓存中添加和移除实例。

doxy1

下面是 Doxygen 声明其缓存的方式:

Doxygen::symbolCache   =new ObjCache(16+cacheSize);// 16 -> room for 65536 elements, 

2. 缓存容量

Doxygen 从配置文件中获取缓存的最大容量:

int cacheSize =Config_getInt("SYMBOL_CACHE_SIZE");

把这个参数做成可配置是很有用的,因为在拥有大量物理内存的机器上,您可以增大缓存容量以提升性能。不过,在较新的 Doxygen 版本中,这个参数已从配置文件中移除,改为使用默认值。

3. 从缓存中释放条目的算法

下面是 Doxygen 源代码中负责在缓存达到上限时释放缓存条目的代码片段:

doxy6

正如

makeResident

方法的代码(注释写得非常好)所示,如果缓存已满,最近最少使用的条目会被移除。

这个方法几乎会被

MemberDef

的所有方法调用。每当需要访问

MemberDef

的状态时都会调用它。它会检查该成员是否已加载,必要时将其加载,并从缓存中移除最近最少使用的成员。

使用缓存的优势

使用缓存可以提升应用程序的性能,但它究竟带来了显著的好处,还是只是一个不值得增加复杂度的微优化?

在我们使用 Clang 作为产品的 C/C++ 解析器之前,第一个版本使用的是 Doxygen 作为解析器。我们对缓存容量做了大量测试:当我们禁用缓存并用这个修改过的版本解析一些 C++ 项目时,解析时间急剧增加,有时从 5 分钟增加到 25 分钟。对于大型项目,这个过程可能耗费数小时,并显著影响系统性能。

结论

在处理大量数据时,使用缓存可以显著提升应用程序的性能。在设计自己应用程序的缓存时,研究开源项目如何实现它们的缓存会非常有帮助。

分享本文