在 C++ 编程中,优化不仅仅是调整代码以获得性能提升。它从根本上围绕着做出明智的设计选择展开。原因如下:
- 算法选择:算法的选择会极大影响性能。使用 O(n log n) 的排序算法而不是 O(n^2) 的算法就是一个典型例子。
- 数据结构:选择合适的数据结构(例如,使用哈希表而不是链表来实现快速查找)可以带来显著的效率提升。
- 内存管理:高效的内存使用以及尽量减少分配/释放操作可以提升性能。内存池或正确使用智能指针等技术可以带来很大改观。
- 并发与并行:设计能够有效利用多线程或多进程的系统可以提升性能。C++11 引入了标准的线程支持,对此有所帮助。
- 避免过早优化:先专注于简洁、可维护的代码,之后再优化关键部分,通常更为有效。
Doxygen 案例研究:通过设计选择对抗内存抖动
当您机器上运行的进程尝试分配超出系统可用容量的内存时,内核会开始在磁盘之间换入换出内存页。这样做是为了释放足够的物理内存,以满足请求方的 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 使用基于磁盘缓存的解决方案;使用磁盘缓存是优化内存使用的流行方式。其思路是将原本需要驻留在内存中的数据存储到磁盘上;这个缓存包含许多槽位,每个槽位保存一份特定的数据,当缓存大小超过某个阈值时,一些槽位将被释放。被释放的数据保存在磁盘上,如果再次需要,它们会被移回内存。
以 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 分钟。对于大型项目,这可能需要数小时,并显著影响系统性能。
结论
有效的 C++ 优化深深植根于做出明智的设计选择。通过选择合适的算法、数据结构和内存管理技术,并在适当之处利用并发,开发者可以实现显著的性能提升。这种战略性的设计方法才是真正驱动 C++ 优化的关键。
