Build Caching
Compiling C++ is computationally expensive. Each Translation Unit (TU) must be preprocessed, parsed Into an Abstract Syntax Tree (AST), optimized, and assembled into machine code. In a typical Workflow, changing a single header file can force the recompilation of hundreds of source files.
Build Caching intercepts compiler calls. It hashes the inputs (source code, compiler flags, and Environment variables). If a hash matches an entry in the local or remote cache, the compilation Step is skipped entirely, and the cached object file is retrieved. This results in zero-cost Compilation for unchanged units.
1. The Caching Landscape
Section titled “1. The Caching Landscape”1.1 CCache (Compiler Cache)
Section titled “1.1 CCache (Compiler Cache)”- Architecture: Local filesystem cache.
- Support: GCC, Clang.
- Platform: Linux, macOS, MinGW.
- Mechanism:
- Preprocessor Mode: Runs the preprocessor (
-E) and hashes the output. Accurate but slower. - Direct Mode: Hashes the source file stats and includes. Fast but requires strict header hygiene.
1.2 Sccache (Shared Compiler Cache)
Section titled “1.2 Sccache (Shared Compiler Cache)”- Maintainer: Mozilla.
- Architecture: Local or Distributed (S3, GCS, Azure, Redis).
- Support: GCC, Clang, MSVC.
- Mechanism: Written in Rust, designed specifically for CI/CD environments where ephemeral build agents need access to a shared cache. It effectively supports Microsoft’s PDB generation constraints.
1.3 BuildCache
Section titled “1.3 BuildCache”- Architecture: Local or remote (HTTP/S3-compatible).
- Support: GCC, Clang, MSVC.
- Mechanism: Written in Rust. Key differentiator: supports remote execution of compiler jobs, not just cache storage. Can distribute compilation across the network.
1.4 Bazel / bazel-remote
Section titled “1.4 Bazel / bazel-remote”- Architecture: Content-addressable cache with remote backends.
- Mechanism: Bazel builds include caching as a first-class concept. The
bazel-remoteproject provides a gRPC/HTTP cache server. Bazel’s caching is fine-grained — it caches individual build actions, not just compiler invocations.
Comparison Table
Section titled “Comparison Table”| Feature | CCache | Sccache | BuildCache | Bazel |
|---|---|---|---|---|
| Language | C/C++ | C/C++, Rust | C/C++, Rust | Any (Starlark) |
| Remote backend | No (local only) | S3, GCS, Azure | HTTP, S3 | gRPC, HTTP |
| MSVC support | No | Yes | Yes | Via rules_msvc |
| Remote execution | No | No | Yes | Yes |
| Cache key | Content hash | Content hash | Content hash | Content hash |
| Precompiled headers | Limited | Yes | Yes | Yes |
| Locking | Filesystem | Internal | Internal | Internal |
2. Installation
Section titled “2. Installation”CCache
Section titled “CCache”# Debian/Ubuntusudo apt install ccache
# Arch Linuxsudo pacman -S ccache
# Fedorasudo dnf install ccachebrew install ccachepacman -S mingw-w64-ucrt-x86_64-ccacheSccache
Section titled “Sccache”Sccache is recommended for Windows (MSVC) users or distributed CI pipelines.
# Via Scoopscoop install sccache
# Via Cargo (Rust Package Manager)cargo install sccache# Via Brewbrew install sccache
# Via Cargocargo install sccache3. How Caching Works: Hash Computation
Section titled “3. How Caching Works: Hash Computation”The cache key is a cryptographic hash of all inputs that affect compilation output. Understanding What goes into this hash is critical for debugging cache misses.
CCache Preprocessor Mode
Section titled “CCache Preprocessor Mode”In preprocessor mode, ccache runs the full preprocessor and hashes the resulting .i file plus the Compiler flags. The hash input is:
This is robust but slow: preprocessing can take 30-50% of total compilation time for heavily Templated C++ code.
CCache Direct Mode
Section titled “CCache Direct Mode”In direct mode, ccache hashes the source file directly and recursively hashes all #includeD Headers using their file content (not preprocessor output). It falls back to preprocessor mode if it Detects macros that might affect the output (e.g., #define in the including file).
Direct mode is significantly faster but can produce false positives (cache hits when the output Would differ) if the compiler’s include resolution differs from ccache’s. The -o flag in direct Mode sets the level of safety.
Proof of Cache Correctness
Section titled “Proof of Cache Correctness”Claim: If the cache key matches, the cached object file is semantically identical to the output Of a fresh compilation.
Proof:
- The cache key is a cryptographic hash (SHA-256 in ccache) of all inputs that affect compilation output: source code content, all included header content, compiler flags, compiler binary path, and the compiler’s output of
--version. - A cryptographic hash function has the property that (collision resistance). In practice, SHA-256 has no known collisions.
- If the cache key for the current compilation matches a stored key, then by collision resistance, all inputs are identical.
- Compilation is a deterministic function of its inputs (source, flags, compiler binary). Therefore, the output must be identical. QED.
Caveat: This proof assumes the compiler itself is deterministic. In practice, compilers can Produce non-deterministic output due to:
- Hash randomization (e.g.,
-frandom-seedor missing-fno-guess-branch-probability). - Parallel compilation with shared file system state.
- Address Space Layout Randomization (ASLR) affecting debug info.
These sources of non-determinism must be controlled for the cache to be correct.
4. CMake Integration
Section titled “4. CMake Integration”Modern CMake (3.4+) supports caching via the <LANG>_COMPILER_LAUNCHER property. This injects the Caching tool command before the compiler command in the build system execution.
Strategy 1: Global Configuration (User-Level)
Section titled “Strategy 1: Global Configuration (User-Level)”Do not modify the project CMakeLists.txt. Instead, modify the CMake Preset or pass the flag During configuration. This ensures developers who lack caching tools can still build the project.
Using CMake Presets (CMakePresets.json):
{ "configurePresets": [ { "name": "linux-clang-ccache", "inherits": "base", "cacheVariables": { "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", "CMAKE_C_COMPILER_LAUNCHER": "ccache" } } ]}Using Command Line:
cmake -S . -B build -DCMAKE_CXX_COMPILER_LAUNCHER=ccacheStrategy 2: Toolchain Injection
Section titled “Strategy 2: Toolchain Injection”If using a toolchain file, you can enforce caching for the specific environment.
find_program(CCACHE_PROGRAM ccache)if(CCACHE_PROGRAM) set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")endif()Strategy 3: Per-Target Caching
Section titled “Strategy 3: Per-Target Caching”Not all targets benefit equally from caching. Frequently-modified targets may not see cache hits at All, while stable dependency targets benefit enormously. You can selectively enable caching:
# Only cache compilation of third-party dependenciesif(NOT PROJECT_IS_TOP_LEVEL) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") endif()endif()5. Configuring Sccache for MSVC
Section titled “5. Configuring Sccache for MSVC”MSVC presents a unique challenge due to PDB (Program Database) generation. Standard PDB Generation (/Zi) is stateful and not thread-safe for distributed caching.
To use Sccache with MSVC effectively:
- Force Embedded Debug Info (
/Z7): This embeds debug info into the.objfiles rather than a separate.pdbduring compilation. - Start the Server: Sccache runs as a background daemon.
CMake Configuration for MSVC/Sccache
Section titled “CMake Configuration for MSVC/Sccache”if(MSVC) # 1. Use /Z7 instead of /Zi to allow caching string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
# 2. Set Launcher find_program(SCCACHE_PROGRAM sccache) if(SCCACHE_PROGRAM) set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}") endif()endif()Running Sccache
Section titled “Running Sccache”Before building, start the daemon:
sccache --start-servercmake --build build6. Architectural Considerations for CI/CD
Section titled “6. Architectural Considerations for CI/CD”In Continuous Integration, build agents start with a clean filesystem. Without a remote cache, ccache is useless because the cache directory is empty.
6.1 Local Cache Restoration (GitHub Actions/GitLab CI)
Section titled “6.1 Local Cache Restoration (GitHub Actions/GitLab CI)”Most CI systems allow saving/restoring directories based on a key.
GitHub Actions Example:
- name: Ccache Restore uses: actions/cache@v3 with: path: ~/.ccache # Key includes OS and Compiler Version to prevent ABI mismatches key: ccache-${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} restore-keys: | ccache-${{ runner.os }}-${{ matrix.compiler }}-6.2 Remote Backend (Sccache)
Section titled “6.2 Remote Backend (Sccache)”For large teams, a centralized S3 bucket ensures that if Developer A compiles a file, Developer B (and the CI agent) gets the cached object immediately.
Configuration: Set environment variables before running the build.
export SCCACHE_BUCKET="my-company-build-cache"export AWS_ACCESS_KEY_ID="..."export AWS_SECRET_ACCESS_KEY="..."
# Sccache automatically detects the S3 config and writes theresccache --start-server6.3 Redis Backend (Sccache)
Section titled “6.3 Redis Backend (Sccache)”For on-premise infrastructure, Redis provides a low-latency cache backend:
export SCCACHE_REDIS="redis://cache.internal:6379"sccache --start-server6.4 Azure Blob Storage
Section titled “6.4 Azure Blob Storage”export SCCACHE_AZURE_BLOB_CONNECTION_STRING="..."sccache --start-server7. Cache Eviction and Size Management
Section titled “7. Cache Eviction and Size Management”Caches grow without bound unless explicitly managed. Each cached object is 100 KB to 10 MB Depending on the translation unit. A large C++ project with 500 TUs can consume 2-5 GB of Cache.
CCache Size Configuration
Section titled “CCache Size Configuration”# Set maximum cache size to 10 GBccache -M 10G
# Set maximum number of filesccache -F 50000
# Clear the cache entirelyccache -C
# View current statisticsccache -sSccache Size Configuration
Section titled “Sccache Size Configuration”# Sccache uses a configurable cache size (default 10 GB)sccache --set-max-size 10GLRU vs Content-Addressable Eviction
Section titled “LRU vs Content-Addressable Eviction”Both ccache and sccache use content-addressable storage: the cache key is a hash of the input, and The value is the output object file. When the cache exceeds its size limit, both tools use an LRU (Least Recently Used) eviction policy. This means infrequently used cache entries are evicted first, Which is generally optimal for development workflows.
Proof that LRU is optimal for development:
- In a development workflow, recently compiled files are the most likely to be recompiled (you are working on them).
- Files that have not been accessed in a long time are unlikely to be recompiled soon.
- LRU evicts the least recently accessed entries, preserving the entries most likely to be needed.
- This is an instance of the stack algorithm (Belady’s optimal page replacement), which is optimal for the class of workloads where future accesses follow a recency pattern. QED.
8. Verification
Section titled “8. Verification”To ensure caching is active and effective, inspect the statistics.
CCache:
ccache -sLook for “Cache Hit Rate”. Ideally, this is >90% on incremental builds.
Sccache:
sccache -sInterpreting CCache Statistics
Section titled “Interpreting CCache Statistics”cache directory /home/user/.ccacheprimary config /home/user/.ccache/ccache.confsecondary config (readonly) /etc/ccache.confstats updated Fri Apr 4 12:00:00 2026cache hit (direct) 1234cache hit (preprocessed) 56cache miss 89cache hit ratio 93.53%called for link 12compile failed 3preprocessor error 1unsupported source language 0no input file 0cleanups performed 0files in cache 10234cache size 4.2 GBmax cache size 10.0 GBKey fields:
- cache hit (direct): Direct mode hit — fastest path, no preprocessing.
- cache hit (preprocessed): Preprocessed mode hit — slower but still avoids compilation.
- cache miss: Input was not in cache, full compilation occurred.
- cache hit ratio: Below 80% suggests something is defeating the cache (see pitfalls).
9. Incremental vs Clean Builds
Section titled “9. Incremental vs Clean Builds”Incremental Builds
Section titled “Incremental Builds”An incremental build recompiles only the translation units whose dependencies have changed. The Build system tracks file modification times and recompiles when a source or header is newer than the Corresponding object file.
Clean Builds (cmake --fresh)
Section titled “Clean Builds (cmake --fresh)”CMake 3.25+ supports cmake --fresh which re-runs the configure step from scratch, then performs a Clean build. This is useful when the build system state is corrupted or when switching branches with Significant CMake changes:
cmake --fresh -S . -B buildcmake --build buildBuild Reproducibility
Section titled “Build Reproducibility”A build is reproducible if the same source code, compiler, and flags produce byte-identical output Every time. Reproducibility is a prerequisite for effective caching:
# Ensure reproducible buildscmake -DCMAKE_CXX_FLAGS="-fdebug-prefix-map=${PWD}=." \ -DCMAKE_C_FLAGS="-fdebug-prefix-map=${PWD}=." \ -B buildKey factors that affect reproducibility:
- Timestamps in debug info: Use
-fdebug-prefix-mapto strip absolute paths. __TIME__and__DATE__: Avoid using these macros; use build-system-provided version info.- Random seeds: Use
-frandom-seed=0or-fno-guess-branch-probability. - File system ordering: Some build systems iterate over files in directory order, which varies across platforms.
Common Pitfalls
Section titled “Common Pitfalls”1. Timestamp Mismatches
Section titled “1. Timestamp Mismatches”If the build system touches files without changing content, ccache might miss. This happens when Build systems regenerate headers or configuration files on every invocation.
Diagnosis: Use ccache -s to monitor hit rates. If the miss rate is unexpectedly high, check For files being regenerated with identical content.
Fix: Use ccache -C to clear the cache and rebuild. Ensure build systems don’t regenerate files Unnecessarily.
2. __TIME__ and __DATE__ Macros
Section titled “2. __TIME__ and __DATE__ Macros”Using __TIME__ or __DATE__ in source code defeats caching because the preprocessor output Changes every second.
// BAD: Changes every compilationconst char* build_time = __TIME__;
// GOOD: Use a build-system-provided definition that only changes when the build ID changesconst char* build_id = BUILD_VERSION;Fix: Pass version information via compiler definitions (-DBUILD_VERSION="1.2.3") that only Change on actual releases.
3. Absolute Paths in Debug Symbols
Section titled “3. Absolute Paths in Debug Symbols”Debug symbols often contain absolute paths. Use -fdebug-prefix-map (GCC/Clang) to map local paths To generic ones to improve cache sharing across different users.
cmake -DCMAKE_CXX_FLAGS="-fdebug-prefix-map=${PWD}=." -B build4. Random or Non-Deterministic Code
Section titled “4. Random or Non-Deterministic Code”Code that includes random elements (UUIDs, timestamps in generated code) produces different output On every compilation. Isolate such code into separate translation units that are not cached.
5. __FILE__ Macro
Section titled “5. __FILE__ Macro”The __FILE__ macro expands to the source file path, which varies between build directories. Use -fmacro-prefix-map to normalize:
cmake -DCMAKE_CXX_FLAGS="-fmacro-prefix-map=${PWD}=src" -B build6. Non-Reproducible Builds
Section titled “6. Non-Reproducible Builds”If different compilers or compiler versions are used (e.g., GCC 12 on one machine, GCC 13 on Another), the cached objects are incompatible. Ensure all cache participants use the same compiler Version.
7. Cache Poisoning
Section titled “7. Cache Poisoning”If a compilation succeeds but produces a corrupted object file (due to a compiler bug, filesystem Error, or OOM during compilation), the corrupted output is cached. Subsequent cache hits will Retrieve the corrupted object. Mitigation:
- Use
ccache -Cto clear the cache when you suspect corruption. - Monitor build failures for patterns that suggest cache corruption.
- Use sccache with a remote backend that can be purged centrally.
10. Distributed Caching Architecture
Section titled “10. Distributed Caching Architecture”For large organizations, a centralized cache provides the greatest benefit. The typical architecture Is:
Developer A ──┐ ┌── CI Agent 1Developer B ──┼── S3 Bucket ──────┼── CI Agent 2Developer C ──┘ (cache backend) └── CI Agent 3Cost Analysis
Section titled “Cost Analysis”Cached compilation costs approximately USD 0.02 per GB per month on S3 (Standard storage). For a 50 GB cache accessed by 100 developers, the monthly storage cost is approximately USD 1.00. The Bandwidth costs for cache reads depend on hit rate and team size, but are under USD 10/month for a medium-sized team. This is negligible compared to developer time saved.
Cache Warmup Strategy
Section titled “Cache Warmup Strategy”When onboarding a new developer or setting up a new CI runner, the cache is cold (empty). To warm The cache:
- Run a full clean build on one machine.
- The cache is now populated with all object files.
- All subsequent builds (by any agent) will hit the cache.
# Full clean build to warm the cachecmake --build build --clean-firstccache -s # Verify cache is populatedCache Partitioning Strategy
Section titled “Cache Partitioning Strategy”For organizations with many independent projects, a single monolithic cache can grow unboundedly. Partition the cache by project or team:
# Per-project cache directoryexport CCACHE_DIR="/mnt/cache/${PROJECT_NAME}"export CCACHE_MAXSIZE=20GThis prevents one project’s cache from evicting another project’s entries.
11. Advanced: CCache Sloppiness
Section titled “11. Advanced: CCache Sloppiness”The CCACHE_SLOPPINESS environment variable relaxes ccache’s strictness, trading correctness for Higher hit rates. Use with extreme caution:
# Allow caching even when __TIME__, __DATE__, or __FILE__ changeexport CCACHE_SLOPPINESS="time_macros,include_file_mtime,include_file_ctime,file_macro"Available sloppiness options:
time_macros: Ignore__TIME__and__DATE__changes.file_macro: Ignore__FILE__path differences.include_file_mtime: Ignore header modification time changes.pch_defines: Ignore differences in precompiled header defines.locale: Ignore locale settings (affects string literals in some locales).system_headers: Don’t hash system headers (risky — system header updates won’t invalidate cache).
When to Use Sloppiness
Section titled “When to Use Sloppiness”Sloppiness should be used only as a last resort. Each option trades correctness for hit rate:
time_macros: Safe if your build timestamps are not embedded in the binary for auditing.file_macro: Safe if you do not use__FILE__for logging or error reporting.system_headers: Dangerous. A system header update (e.g., glibc security patch) will not invalidate the cache, potentially building against an outdated header.
12. CCache Configuration File
Section titled “12. CCache Configuration File”For persistent configuration, create ~/.ccache/ccache.conf:
max_size = 10Gmax_files = 50000temporary_dir = /tmp/ccache-tmpcompression = truecompression_level = 6The compression option reduces disk usage at the cost of slight CPU overhead. For SSD-based Systems, the I/O savings outweigh the compression cost.
CCache Environment Variables
Section titled “CCache Environment Variables”| Variable | Purpose | Default |
|---|---|---|
CCACHE_DIR | Cache storage directory | ~/.ccache |
CCACHE_MAXSIZE | Maximum cache size | 5G |
CCACHE_MAXFILES | Maximum number of cache files | Unlimited |
CCACHE_TEMPDIR | Temporary directory for in-progress operations | CCACHE_DIR/tmp |
CCACHE_COMPRESS | Enable/disable compression | false |
CCACHE_COMPRESSLEVEL | Compression level (1-9) | 6 |
CCACHE_SLOPPINESS | Relax correctness checks for higher hit rates | (empty) |
CCACHE_DEBUG | Enable debug logging | (disabled) |
CCACHE_LOGFILE | Log file path | (stderr) |
CCACHE_NOHASHDIR | Ignore directory components in the hash | false |
CCACHE_PREFIX_KEY | Additional hash key (e.g., compiler flags) | (empty) |
CCACHE_BASEDIR | Base directory for path normalization | (empty) |
CCACHE_DISABLE | Disable ccache entirely (pass through to compiler) | false |
CCache with Precompiled Headers
Section titled “CCache with Precompiled Headers”Precompiled headers (PCH) complicate caching because the PCH file depends on the same headers as the TU, and the cache key must account for this dependency. Ccache supports PCH caching with the pch_defines sloppiness option and by detecting #include of PCH files:
# Enable PCH-aware cachingexport CCACHE_SLOPPINESS="pch_defines"Without this sloppiness, changes to the PCH defines may not properly invalidate the cache for TUs That include the PCH.
13. Sccache Advanced Configuration
Section titled “13. Sccache Advanced Configuration”Sccache Statistics
Section titled “Sccache Statistics”sccache -sOutput example:
Compile requests 1234Compile requests executed 800Cache hits 600Cache misses 200Cache timeouts 0Cache read errors 0Forced recaches 0Cache write errors 0Compilation failures 5Cache errors 0Average cache write rate 12.5 MiB/sAverage cache read rate 85.2 MiB/sCache location Local disk: /home/user/.cache/sccacheSccache and Rust
Section titled “Sccache and Rust”Sccache also caches Rust compilation (cargo). This makes it useful for mixed C++/Rust projects:
export RUSTC_WRAPPER=sccachecargo buildSccache Debugging
Section titled “Sccache Debugging”When sccache produces unexpected cache misses, enable debug logging:
SCCACHE_LOG=debug sccache --start-server# Check the log:journalctl --user -u sccache # or check stderrCommon causes of unexpected misses:
- Different compiler flags between invocations (e.g., different
-Ddefinitions). - Different compiler binary path (e.g.,
/usr/bin/ccvs/usr/bin/gcc). - File content differences in headers (even whitespace changes).
- Environment variables that affect compilation (e.g.,
LC_ALL).
14. Build Caching and Reproducibility
Section titled “14. Build Caching and Reproducibility”Reproducible Builds: Formal Requirements
Section titled “Reproducible Builds: Formal Requirements”A build is reproducible if, given the same source code, compiler, flags, and toolchain, the output Is byte-identical every time. This is a prerequisite for meaningful caching in a distributed Environment.
The following conditions must hold for reproducible builds:
- Deterministic compiler output: The compiler must produce the same object file given the same inputs. Use
-frandom-seed=0to disable randomization in GCC/Clang. - Deterministic file ordering: Filesystem iteration order must not affect the build. Use
-DCMAKE_EXPORT_COMPILE_COMMANDS=ONto ensure deterministic TU ordering. - No timestamps in output: Avoid
__TIME__``__DATE__And similar macros. Use build-system- provided version definitions. - No absolute paths in debug info: Use
-fdebug-prefix-mapand-fmacro-prefix-map. - Same compiler version: All cache participants must use the exact same compiler version.
Verifying Reproducibility
Section titled “Verifying Reproducibility”# Build twice and comparecmake --build build1cp -r build1 build2rm -rf build2/**/*.ocmake --build build2diff <(find build1 -name '*.o' -exec md5sum {} \; | sort) \ <(find build2 -name '*.o' -exec md5sum {} \; | sort)If the diff is empty, the builds are reproducible. Any difference indicates a source of Non-determinism that will cause cache misses in a distributed environment.
See Also
Section titled “See Also”Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to build caching, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.