当您机器上运行的进程试图分配超出系统可用容量的内存时,内核就会开始在磁盘之间换入换出内存页。这样做是为了释放足够的物理内存,以满足请求方的 RAM 分配需求。
过度使用交换被称为颠簸(thrashing),这是我们不希望看到的,因为它会降低系统的整体性能——主要原因是硬盘比 RAM 慢得多。
如果您的应用程序需要处理大量数据,它就可能遭遇颠簸问题,速度可能会急剧下降。解决方案有两种:要么优化应用程序以更有效地使用内存,要么为系统增加更多物理内存。
让我们来看看Doxygen采用了哪种方案来优化内存使用、避免颠簸问题。
Doxygen 是从带注释的 C++ 源代码生成文档的事实标准工具,但它也支持其他流行的编程语言,如 C、Objective-C、C#、PHP、Java、Python 等。非常感谢 Dimitri van Heesch 为开发和维护该项目付出的巨大努力。
Doxygen 以源文件为输入,解析它们以提取所需数据,并将结果存储在 DirDef、FileDef、NamespaceDef、ClassDef 和 MemberDef 等类的实例中。它们都继承自 Definition 类。

这些类的实例随后会被用来生成文档。消耗内存最多的数据是关于方法和变量的信息,它们由 MemberDef 类表示。这些实例的大小可能会增长到超过 1 GB,具体取决于所处理项目中方法和变量的数量。
对于某些项目,将所有这些实例存储在内存中会影响系统性能,生成文档可能需要好几个小时。
Doxygen 是如何优化内存的?
Doxygen 使用基于缓存的解决方案;使用缓存(cache)是优化内存使用的流行方式。其思路是将需要驻留内存的数据存储在缓存中。这个缓存包含许多槽位,每个槽位保存一份特定的数据。当缓存超过一定大小时,一些槽位会被释放。被释放的数据会移动到磁盘上,如果再次需要它,就再把它移回内存。
在 Doxygen 中,算法非常简单:
- 定义一个拥有 65535 个槽位的缓存。
- 当需要创建一个 MemberDef 实例时,Doxygen 检查缓存槽位是否可用。如果可用,就在内存中创建实例;否则,将它存储在磁盘上的数据文件中,并更新一个索引文件,记录这份数据在数据文件中的存储位置。
- 如果 Doxygen 需要访问某个 MemberDef 实例,它会检查该实例是否存在于缓存中。如果不存在,Doxygen 就从索引文件读取其数据的存储位置,在数据文件中定位到该位置,并从磁盘加载它。
缓存的性能取决于:
- 容器:可以是队列、数组、链表,或者自定义容器。容器的选择会影响缓存性能。
- 缓存的最大容量。
- 从缓存中释放条目的算法。当缓存达到上限时,您必须决定释放哪些条目。例如,您可以:
- 释放最先加载的槽位。
- 释放最后加载的槽位。
- 释放使用最少的槽位。
1. 容器
Doxygen 定义了 ObjCache 类,它是一个由 CacheNode 对象组成的链表。这个类负责向缓存中添加和移除实例。

下面是 Doxygen 声明其缓存的方式:
Doxygen::symbolCache =new ObjCache(16+cacheSize);// 16 -> room for 65536 elements, 2. 缓存容量
Doxygen 从配置文件中获取缓存的最大容量:
int cacheSize =Config_getInt("SYMBOL_CACHE_SIZE");把这个参数做成可配置的是个好主意,这样如果您的机器拥有大量物理内存,就可以增大缓存以提升缓存性能。不过,在较新的 Doxygen 版本中,这个参数已从配置文件中移除,改为使用默认值。
3. 从缓存中释放条目的算法
下面是 Doxygen 源代码中负责在缓存达到上限时释放缓存条目的代码片段:

正如
makeResident方法的代码(注释写得非常好)所示,如果缓存已满,最近最少使用的条目会被移除。
这个方法几乎在
MemberDef的所有方法中都会被调用;每当您需要访问
MemberDef的状态时都会调用它,以检查该成员是否已加载,若未加载则将其加载,并从缓存中移除最近最少使用的成员。
使用缓存的影响
使用缓存可以提升应用程序的性能,但它究竟带来了显著的优化,还是只是一个不值得加入应用程序的微优化?
在我们使用 Clang 作为产品的 C/C++ 解析器之前,第一个版本使用的是 Doxygen 作为解析器。我们对缓存容量做了大量测试:当我们禁用缓存并用这个修改过的版本解析一些 C++ 项目时,解析时间急剧增加,有时从 5 分钟增加到 25 分钟。对于大型项目,可能耗费数小时,并显著影响系统性能。
结论
在处理大量数据时,使用缓存是提升应用程序性能的有力手段。在设计自己应用程序的缓存时,研究开源项目如何实现它们的缓存会非常有帮助。
