When the processes running on your machine attempt to allocate more memory than your system has available, the kernel begins to swap memory pages to and from the disk. This is done to free enough physical memory to satisfy the requester’s RAM allocation requirements.
Excessive use of swapping is called thrashing and is undesirable because it lowers overall system performance, mainly because hard drives are far slower than RAM.
If your application needs to process a large amount of data, it may be exposed to thrashing and could slow down dramatically. Two solutions exist: either optimize your application to use memory more efficiently, or add more physical RAM to the system.
Let’s explore the solution Doxygen uses to optimize memory usage and avoid thrashing.
Doxygen is the de facto standard tool for generating documentation from annotated C++ sources, but it also supports other popular programming languages such as C, Objective-C, C#, PHP, Java, Python, and many others. Thanks to Dimitri van Heesch for his great effort in developing and maintaining the project.
Doxygen takes the source files as input, parses them to extract the needed data, and stores the result in class instances of the DirDef, FileDef, NamespaceDef, ClassDef, and MemberDef kinds. All of them inherit from the Definition class.

The instances of these classes will then be used to generate the documentation. The data that consumes the most memory is information about methods and variables, represented by the MemberDef class. The total size of these instances can grow to more than 1 GB, depending on the number of methods and variables in the projects being processed.
For some projects, storing all these instances in memory can affect system performance, and generating the documentation may take many hours.
How does Doxygen optimize memory?
Doxygen uses a cache-based solution; using a cache is a popular way to optimize your memory usage. The idea is to keep the data that needs to reside in memory in a cache. The cache contains many slots, each holding a specific piece of data, and some slots are released when the cache exceeds a certain size. The released data is moved to disk, and if we need it again, it is moved back to memory.
In the case of Doxygen, the algorithm is very simple:
- Define a cache with 65,535 slots.
- When a MemberDef instance needs to be created, Doxygen checks whether a cache slot is available. If so, the instance is created in memory; otherwise, it is stored in a data file on disk, and an index file is updated to record its location in that file.
- When Doxygen needs to access a MemberDef instance, it checks whether the instance is present in the cache. If it is not present, Doxygen uses the index file to determine where the data is stored, seeks to that position in the data file, and loads it from disk.
The performance of the cache depends on:
- The container: it could be a queue, an array, a list or maybe a custom container. The choice of container can affect cache performance.
- The maximum size of the cache.
- The algorithm used to free entries from the cache. When the cache reaches its maximum size, you must decide which entries to release. For example, you could:
- Release the first slots loaded.
- Release the last slots loaded.
- Release the least-used slots.
1. The Container
Doxygen defines the ObjCache class, which is a linked list of CacheNode objects; this class is responsible for adding and removing instances from the cache.

Here’s how Doxygen declares its cache:
Doxygen::symbolCache =new ObjCache(16+cacheSize);// 16 -> room for 65536 elements, 2. Cache size
Doxygen gets the maximum cache size from the configuration file:
int cacheSize =Config_getInt("SYMBOL_CACHE_SIZE");Making this parameter configurable is useful because it allows you to increase the cache size on machines with plenty of physical memory, thereby improving cache performance. However, in the newer Doxygen releases, this parameter has been removed from the configuration file, and a default value is used.
3. The algorithm for releasing entries from the cache
Here is the Doxygen source-code snippet responsible for releasing cache entries when the cache reaches its maximum size:

As specified in the makeResident method code, which is very well commented, the least recently used item is removed if the cache is full.
This method is invoked for almost all MemberDef methods, it’s called each time you have to access the MemberDef state to check if this member is loaded or not. If it is not loaded, Doxygen loads it and removes the least recently used member from the cache.
The impact of using the cache
Using a cache can improve application performance, but does caching provide a significant optimization, or is it merely a micro-optimization that is not worth the added complexity?
Before using Clang as the C/C++ parser for our product, we used Doxygen as the parser for our first version, and we ran many tests on cache size. When we disabled the cache and parsed some C++ projects with this modified version, the parsing time increased significantly — sometimes from 5 minutes to 25 minutes. For large projects, it took hours and had a significant impact on system performance.
Conclusion
Using a cache can be a powerful way to improve application performance when handling large amounts of data. Exploring how open-source projects implement caching can be very useful when deciding how to implement it in your own applications.
