Introduction
Previously and even today, only implicit module builds were (and still are) supported for Clang modules exclusively. The new -fmodules-driver adds support for explicit module builds for both Clang modules and C++ named modules. However, one of the main advantages of modules is their ability to being cached. Currently, -fmodules-driver does not support this. Therefore, the RFC strives towards an effort to propose an efficient, correct and safe filesystem cache which integrates seamlessly with Clang flags using the -fmodules-driver mode.
The Modules Cache System at Command Line Interface(CLI) level
To illustrate how this feature will look like at CLI level, let us take an example.
CLI Command:
clang++ -std=c++23 main.cpp A.cpp -fmodules-driver -fmodules -fmodule-map-file=module.modulemap -o result.out -fmodules-driver-cache-path=prebuilt -fmodules-prune-interval=604800
Files corresponding to the above command:
// main.cpp
#include "MyLib.h"
import std;
import A;
auto main() -> int {
std::println("{}", make_greeting("modules"));
std::println("The answer is: {}", get_answer());
}
// A.cpp
export module A;
import std;
export auto make_greeting(std::string_view Name) -> std::string {
return std::format("Hello, {}!", Name);
}
// module.modulemap
module MyLib {
header "MyLib.h"
export *
}
// MyLib.h
auto get_answer() -> int { return 42; }
When the CLI command is executed, the following cache entries will be populated in cache:
ls prebuilt/*/*.{pcm,o,out}
# prebuilt/1AYBIGPM8R2GA/main.o
# prebuilt/3L1K4LUA6O31B/A.pcm
# prebuilt/3L1K4LUA6O31B/A.o
# prebuilt/VH0YZMF1OIRK4/MyLib.pcm
# prebuilt/VH0YZMF1OIRK4/MyLib.o
# prebuilt/7Q9X2CPL5M8TZ/result.out
Deep Dive into the Internals of The Modules Cache System
The Modules Cache System will have 3 major areas to tackle when implementing it:
- File System Cache
- Cache Concurrency Control
- Integrating The Modules Cache System with Clang flags
File System Cache
At present, only generation of valid ContextHash for Clang modules is supported. With the new File System Cache, we are going to support generation of valid ContextHash for Clang modules, C++ named modules and non-modules.
File System Cache Structure
There are 3 types of nodes in the cache file system tree:
cache-rootdirectory: It is the root directory of our cache structure.<CompilationGraph_NodeType>_ContextHash_Ndirectories: the directories which are named after the cache key of their corresponding source/module/non-module file. They store the prebuilt artifacts and metadata corresponding to a particular source/module/non-module file.- artifact files: the various prebuilt artifacts. Examples:
.pcm,.o,.outfiles.
Algorithm to detect changes in dependencies and make build/rebuild decisions
The below described algorithm is effective in detecting any changes/additions in any node or edge of CompilationGraph( More details about CompilationGraph data structure can be found in the merged PR #152770).
Determining ContextHash using Recursive Hashing: We are going to traverse the CompilationGraph recursively in top-down fashion(i.e., from parent nodes to subsequent children nodes) starting from the node RootNode(we are not going to generate ContextHash for RootNode).
For each JobNode that we encounter, its corresponding ContextHash computation may include 3 hash values:
- HashOfContents: It is the hash of the contents of that JobNode. Contents include - source/module file name, textually included header files and any driver arguments that can affect emitted output after compilation of that file. [Note: we will get information about ClangModuleDeps or NamedModuleDeps through HashOfParent.]
- HashOfParent: ContextHash of JobNode’s parent JobNode(s). This will be passed down recursively from parent JobNode(s) to a child JobNode.
- CombinedHash: It is the hash obtained after combining HashofContents and all HashOfParent(s).
There can be 2 types of JobNodes depending on its parent node:
- JobNode with only RootNode as parent: in this case, its ContextHash is just HashofContents.
- JobNode with at least one other JobNode as parent: in this case, its ContextHash is CombinedHash.
Build/Rebuild Decision & Storing Cache Entries: After generating the ContextHash of a particular JobNode, we have to make a build/rebuild decision. Inside the cache-root directory, if we are unable to find the directory with ContextHash as name(i.e., the <CompilationGraph_NodeType>_ContextHash_N directory), we need to build/rebuild.
If we need to build/rebuild, execute the -cc1 job. Store its emitted output in a path determined by cache-root and ContextHash of that JobNode.
[Note: If a parent JobNode was rebuilt, the decision to rebuild the children JobNode(s) will be automatically propagated. This is because when the ContextHash of a parent JobNode changes, the ContextHash of its children JobNode(s) will also change due to top-down chaining of HashOfParent(s) in recursion.]
I would need to implement the method:
CacheLookUp for writing/reading/touching to and from cache. The filesystem cache can lookup a particular artifact file using cache-root path, ContextHash and name of the artifact file.
Cache Concurrency Control
Flowchart that explains the proposed cache concurrency control system:
Important factors to consider:
- In a particular process, a file must be built at most one time.
- Locks are acquired per file being built.
- Cache management and actual compilation should be dealt separately.
- Maintaining cache correctness should be the utmost priority. We have to maintain correctness even at the cost of duplicate builds.
Integrating The Modules Cache System with Clang flags
-fmodules-driver-cache-path
The -fmodules-driver-cache-path flag is a new flag we are going to introduce. This is because we are, for the first time, adding support for caching C++ named modules and non-modules. The -fmodules-driver-cache-path flag works in a similar fashion as -fmodules-cache-path as described in the Clang Modules documentation under Command-line parameters section.
-fmodules-prune-interval and -fmodules-prune-after
Let’s take the example files from The Modules Cache System at Command Line Interface(CLI) level and the case of -fmodules-prune-interval CLI command. The command is executed and cache entries are populated.
Assume at some frame of time between 0 seconds and default delay, module MyLib was modified. Say, its ContextHash changes from VH0YZMF1OIRK4 to K4N8Z1WQ7R2XM. Then due to recursive chaining, ContextHashes for main.cpp artifacts and result.out will also change. New cache entries will be added after a fresh execution of -cc1 jobs:
ls prebuilt/*/*.{pcm,o,out}
# prebuilt/1AYBIGPM8R2GA/main.o
# prebuilt/3L1K4LUA6O31B/A.pcm
# prebuilt/3L1K4LUA6O31B/A.o
# prebuilt/VH0YZMF1OIRK4/MyLib.pcm
# prebuilt/VH0YZMF1OIRK4/MyLib.o
# prebuilt/7Q9X2CPL5M8TZ/result.out
# prebuilt/K4N8Z1WQ7R2XM/MyLib.pcm
# prebuilt/K4N8Z1WQ7R2XM/MyLib.pcm
# prebuilt/P9T3L6YV0H5DA/main.o
# prebuilt/X2M7Q4C8B1ZRL/result.out
From the above files listing, we see that artifacts inside prebuilt/1AYBIGPM8R2GA, prebuilt/VH0YZMF1OIRK4 and prebuilt/7Q9X2CPL5M8TZ became obsolete. After the default delay, -fmodules-prune-interval will take an attempt and try to remove these obsolete directories and files. If successful, the obsolete cache entries will be purged:
ls prebuilt/*/*.{pcm,o,out}
# prebuilt/3L1K4LUA6O31B/A.pcm
# prebuilt/3L1K4LUA6O31B/A.o
# prebuilt/K4N8Z1WQ7R2XM/MyLib.pcm
# prebuilt/K4N8Z1WQ7R2XM/MyLib.pcm
# prebuilt/P9T3L6YV0H5DA/main.o
# prebuilt/X2M7Q4C8B1ZRL/result.out

