博客 阅读时间 5 分钟

颠簸对 C++ 性能的影响:Doxygen 分析

分享本文
Thrashing Impact on C++ Performance: Doxygen Analysis

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

过度使用交换被称为颠簸(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 使用了一种基于缓存的解决方案;使用缓存是优化内存使用的常用方法。其思路是:把需要驻留在内存中的数据保存在缓存里。缓存包含许多槽位,每个槽位存放一份特定的数据;当缓存超过一定大小时,一些槽位就会被释放。被释放的数据会被转移到磁盘上,如果再次需要,再把它移回内存。

以 Doxygen 为例,其算法非常简单:

  • 定义一个包含 65,535 个槽位的缓存。
  • 当需要创建一个 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的状态、检查该成员是否已加载时,它都会被调用。如果尚未加载,Doxygen 会加载它,并从缓存中移除最近最少使用的成员。

使用缓存带来的影响

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

在我们把 Clang 用作产品的 C/C++ 解析器之前,第一个版本曾使用 Doxygen 作为解析器,我们对缓存容量做了大量测试。当我们禁用缓存、用这个修改过的版本解析一些 C++ 项目时,解析时间显著增加——有时从 5 分钟变成 25 分钟。对于大型项目,解析要花上数小时,并且对系统性能有显著影响。

结语

在处理大量数据时,使用缓存可以成为提升应用程序性能的有力手段。在决定如何在自己的应用程序中实现缓存时,研究开源项目是如何实现缓存的会非常有用。

分享本文