vix Object Cache and Incremental Builds
C++ builds become slow when the build system repeats work that did not need to happen.
The compiler may be fast enough for one file.
The problem appears when the build system recompiles too much:
unchanged source files
unchanged headers
unchanged compiler flags
unchanged dependencies
unchanged build configurationThe purpose of an object cache is simple:
if this exact source file was already compiled with the exact same inputs,
reuse the object file instead of compiling it againFor vix build, this is one of the most important foundations for faster incremental builds.
What an object file is
In a normal C++ build, each source file is usually compiled into an object file.
Example:
src/main.cpp -> main.o
src/app.cpp -> app.oThen the linker combines object files into the final output:
main.o + app.o -> myappSo if only src/app.cpp changed, the ideal incremental build is:
recompile app.cpp
reuse main.o
relink myappIt should not recompile every source file.
The basic incremental build model
A simple project:
myapp/
include/
myapp/
app.hpp
src/
main.cpp
app.cppBuild graph:
src/main.cpp -> main.o
src/app.cpp -> app.o
main.o + app.o -> myappIf src/app.cpp changes:
main.o stays valid
app.o becomes dirty
myapp must relinkA good build system should rebuild only:
app.o
myappnot:
main.oWhy object caching matters
Incremental builds already avoid some work.
But an object cache goes further.
It can reuse compiled objects even when the build directory was cleaned, recreated, or moved, as long as the compile inputs are identical.
Example:
first build:
compile src/main.cpp -> main.o
store main.o in object cache
later build:
same source
same headers
same flags
same compiler
restore main.o from cacheThis turns repeated compilation into a file restore.
Object cache vs normal incremental build
A normal incremental build asks:
Is the existing object file in the build directory still valid?An object cache asks:
Have I ever built this exact object before?That distinction matters.
If the build directory is removed:
rm -rf build-ninjaa normal incremental build loses its objects.
But an object cache may still restore them.
Correctness comes first
An object cache must be conservative.
A wrong cache hit is worse than a slow build.
If Vix is unsure whether an object file is valid, it should recompile.
The rule should be:
reuse only when the full compile identity matchesThe cache key must include every input that can affect the object file.
Compile identity
The compile identity is the full set of information that makes one object file valid.
Important inputs include:
source file content
header dependencies
compiler executable
compiler version
compiler arguments
include directories
preprocessor definitions
C++ standard
target triple
build type
toolchain
system headers when relevantIf any of these changes, the cached object may no longer be valid.
Source content
The source file is the obvious input.
If this changes:
src/app.cppthe object file must be rebuilt.
A source file can be checked through:
timestamp
file size
content hashFor cache keys, content hashes are safer.
Header dependencies
Headers are just as important as source files.
Example:
#include <myapp/app.hpp>If include/myapp/app.hpp changes, then src/app.cpp may need to be recompiled.
This is why the object cache cannot rely only on the .cpp file.
It must know the dependency set.
Dependency files
Compilers can generate dependency files.
Example:
app.o: src/app.cpp include/myapp/app.hpp include/myapp/config.hppThese files tell Vix which headers affect an object file.
If a header changes, Vix can mark the correct object files as dirty.
Without dependency files, the cache would either be unsafe or too conservative.
Compiler command
The compiler command is part of the object identity.
This command:
g++ -std=c++20 -Iinclude -c src/app.cpp -o app.ois not the same as:
g++ -std=c++23 -Iinclude -DDEBUG=1 -c src/app.cpp -o app.oEven if the source file is unchanged, the output object may differ.
So Vix needs a command hash.
Compiler identity
The compiler itself matters.
These are not necessarily equivalent:
g++ 13
g++ 14
clang++ 18A cache entry produced by one compiler version may not be safe for another compiler version.
The object cache key should include compiler identity.
At minimum:
compiler path
compiler version
target tripleBuild type
Debug and release builds are different.
A debug object may be compiled with:
-g
-O0A release object may be compiled with:
-O3
-DNDEBUGThose outputs are not interchangeable.
The build type or full compiler command must be part of the cache key.
Include directories
Include directories affect which headers are found.
This command:
g++ -Iinclude -c src/app.cppcan produce a different result from:
g++ -Iother_include -c src/app.cppEven if the source file text is the same, the compiler may read different headers.
Include paths must be part of the compile identity.
Defines
Preprocessor definitions affect compilation.
Example:
defines = [
MYAPP_ENABLE_LOGGING=1,
]This can change compiled code.
So definitions must be included in the cache key.
C++ standard
The selected standard changes how code is parsed and compiled.
standard = c++20is not equivalent to:
standard = c++23The C++ standard must be part of the object identity.
Target triple
Cross-compilation changes the output.
A cached object for:
x86_64-linux-gnucannot be reused for:
aarch64-linux-gnuThe target triple must be part of the cache key.
Object cache flow
A compile task can follow this flow:
1. compute compile identity
2. look for object in cache
3. if cache hit:
restore object file
restore dependency file if available
4. if cache miss:
run compiler
store object file in cache
store dependency file in cacheThis gives a simple and safe model.
Cache hit
A cache hit means:
Vix found a previously compiled object with the same compile identity.Then Vix can copy or restore:
cached object -> build output object
cached dependency file -> build dependency fileThis avoids running the compiler.
Cache miss
A cache miss means:
no safe cached object existsThen Vix runs the compiler.
After successful compilation, Vix stores the result for future use.
Cache misses are normal.
They happen when code changes, flags change, or a project is built for the first time.
Cache invalidation
The object cache must invalidate entries when relevant inputs change.
Examples:
source changed
header changed
compiler changed
flags changed
defines changed
include_dirs changed
target triple changed
build type changed
toolchain changedThe easiest safe model is content-addressed caching.
The key is derived from the full compile identity.
When identity changes, the key changes.
Incremental build without object cache
Without an object cache, incremental build relies on the current build directory.
Example:
build-ninja/
main.o
app.oIf app.cpp changes:
app.o rebuilds
main.o staysThis is good, but local to the build directory.
If the build directory disappears, the work is lost.
Incremental build with object cache
With an object cache:
~/.vix/cache/objects/or another cache location can store compiled outputs.
Then Vix can restore objects even after:
clean build
new build directory
reconfigured project
CI cache restoreas long as the compile identity matches.
Relationship with ccache
Tools like ccache already cache compiler outputs.
So why should Vix have an object cache?
Because Vix can integrate caching into the build graph.
Vix can know:
which target is being built
which task is dirty
which dependency caused the rebuild
which object was restored
which artifact can be skippedccache works at the compiler invocation layer.
Vix can work at the build orchestration layer.
They can also complement each other.
Object cache and BuildGraph
The object cache becomes more powerful when connected to the BuildGraph.
The graph knows:
compile tasks
source inputs
header dependencies
object outputs
link inputs
target outputsThe object cache knows:
which compile outputs can be restoredTogether, they allow Vix to skip more work safely.
Object cache and Scheduler
The scheduler can treat a cache restore as a task result.
For each compile task:
if object cache hit:
restore object
else:
compile sourceIndependent restores and compiles can still happen in parallel.
The scheduler does not need to care whether a task ran the compiler or restored a cached object.
It only needs the output to become valid.
Object cache and Link
Even if all objects are restored from cache, the final link may still be needed.
But link can be skipped if the final executable or library is also known to be valid.
That is where the artifact cache or link cache becomes useful.
Object cache handles object files.
Artifact cache handles larger outputs.
Example: one file changed
Project:
src/main.cpp
src/app.cpp
src/server.cppChange:
src/app.cppExpected behavior:
main.o -> cache hit or clean
app.o -> compile
server.o -> cache hit or clean
myapp -> relinkThis is the basic incremental build experience.
Example: header changed
Project:
src/main.cpp includes include/app.hpp
src/server.cpp includes include/server.hppChange:
include/app.hppExpected behavior:
main.o -> dirty
server.o -> clean
myapp -> relinkThe dependency file tells Vix which object depends on which header.
Example: define changed
Manifest changes:
defines = [
ENABLE_LOGGING=1,
]to:
defines = [
ENABLE_LOGGING=0,
]Expected behavior:
affected compile tasks become dirty
objects rebuild or cache miss
target relinksEven if source files did not change, the object identity changed.
Example: compiler changed
Compiler changes from:
g++ 13to:
clang++ 18Expected behavior:
object cache keys change
old objects are not reused
project rebuilds safelyThis is correct.
Example: clean build with warm cache
First build:
compile main.cpp
compile app.cpp
link myapp
store objectsThen:
rm -rf build-ninja
vix buildWith warm object cache:
restore main.o
restore app.o
link myappIf artifact cache also has the final binary, even linking may be skipped.
No-op build
A no-op build means nothing changed.
Ideal behavior:
configuration unchanged
graph unchanged
objects unchanged
target unchanged
finish immediatelyObject cache is not always needed for no-op builds if the build directory is already valid.
But object cache helps when build outputs are missing.
Cache storage
A cache entry can store:
object file
dependency file
metadata
compile identity
source hash
dependency hashes
compiler info
command hash
created timeThe metadata helps Vix explain cache behavior.
Example:
cache hit: src/app.cpp
cache miss: command hash changed
cache miss: header include/config.hpp changedExplainability matters.
Cache keys
A safe cache key should be stable and content-based.
Conceptually:
hash(
source content,
dependency contents,
compiler identity,
compiler arguments,
include directories,
defines,
standard,
target triple,
build type
)The exact implementation can evolve.
The important rule is:
same key means same compile resultor at least close enough to be safe under the build model.
Cache metadata
Metadata can include:
source path
object path
compiler
arguments hash
dependencies
dependency hashes
target triple
build typeThis is useful for debugging.
When a cache miss happens, Vix can eventually explain why:
miss: compiler changed
miss: header hash changed
miss: command hash changedCache eviction
A cache can grow over time.
Eventually, Vix may need cache eviction.
Eviction policies can use:
least recently used
maximum cache size
maximum age
manual clean command
per-project cleanupThe first priority is correctness.
Eviction is a storage management problem, not a build correctness problem.
Cache and CI
Object cache can help CI when cache directories are restored between runs.
A CI pipeline can restore:
~/.vix/cache/Then repeated builds can reuse objects.
But CI environments vary.
Compiler paths, compiler versions, system headers, and target triples can differ.
The cache key must account for those differences.
Cache and distributed builds
A deeper future version could share cache entries between machines.
That requires stronger identity checks.
Important inputs become even more critical:
compiler version
target triple
system headers
standard library
toolchain
environmentDistributed cache is powerful, but it must be conservative.
Object cache and vix.app
vix.app makes object caching easier to reason about.
The manifest directly lists:
sources
include_dirs
defines
compile_options
standard
target typeThat gives Vix structured compile input before it even asks CMake.
Today, Vix can use generated CMake and imported compile commands.
Later, Vix can create compile tasks directly from vix.app.
Native vix.app build path
The future native path is:
vix.app
-> BuildGraph
-> ObjectCache
-> Scheduler
-> LinkIn this model, object caching is not an add-on.
It is part of the build execution model.
For each source file, Vix can decide:
restore object
or compile sourcebefore scheduling link.
Object cache and CMake projects
For arbitrary CMake projects, Vix should not guess.
It should import:
compile_commands.json
build.ninja
dependency filesThen cache decisions are based on real generated build information.
This keeps compatibility with complex CMake logic.
Object cache and correctness boundary
Some inputs are hard to capture perfectly.
Examples:
system headers
compiler plugins
environment variables
implicit include paths
generated headers
precompiled headersIf these are not modeled, Vix must be conservative.
A build cache should prefer a miss over a wrong hit.
Precompiled headers
Precompiled headers complicate object caching.
They introduce additional binary inputs.
If Vix supports them deeply, the cache identity must include:
PCH source
PCH flags
compiler identity
dependent object relationshipFor simple initial object caching, Vix can avoid special PCH support or rely on CMake/Ninja behavior.
Generated headers
Generated headers must be handled carefully.
If a source depends on a generated header, the object is valid only after the generated header is up to date.
The graph must represent:
generate header
compile sourcewith the correct ordering.
This is easier in CMake compatibility mode because Ninja already knows the generated dependency graph.
For native vix.app, Vix would need explicit support.
Link invalidation
Object cache does not eliminate linking.
If any object changes, the linked target usually becomes dirty.
So the full incremental build path is:
restore or compile objects
then relink if link inputs changedA separate artifact cache can optimize final outputs.
Artifact cache vs object cache
Object cache:
source -> objectArtifact cache:
objects/libs/config -> executable/library/packageObject cache is fine-grained.
Artifact cache is coarse-grained.
Both are useful.
A deep build system needs both.
Measuring object cache effectiveness
Useful metrics include:
compile tasks selected
object cache hits
object cache misses
objects restored
objects compiled
time spent restoring
time spent compiling
link time
total build timeVix should eventually expose this in verbose output.
Example:
objects: 18 selected, 16 cache hits, 2 compiledThis helps engineers trust the cache.
Explaining rebuilds
Fast builds are useful.
Explainable builds are better.
Vix should eventually answer:
Why did this file rebuild?Example explanations:
src/app.cpp rebuilt because source changed
src/main.cpp rebuilt because include/config.hpp changed
src/server.cpp rebuilt because compile options changedThis is a major developer experience improvement.
Common object cache mistakes
Reusing objects across compilers
Wrong:
reuse g++ object for clang++ buildCorrect:
compiler identity is part of cache keyIgnoring headers
Wrong:
cache key = source hash onlyCorrect:
cache key includes dependency headersIgnoring compile flags
Wrong:
same source means same objectCorrect:
source + command + dependencies define object identityTrusting timestamps only
Timestamps are useful, but cache keys should be stronger.
For correctness, use content-based identity where it matters.
Practical first version
A practical first object cache can support:
compile command hash
source hash
dependency file hash
compiler identity
object output
dependency outputFlow:
read compile_commands.json
read dependency file if present
compute key
restore object on hit
compile on miss
store object after successThis is enough to create real value.
Later improvements
Later improvements can include:
better dependency hashing
system header modeling
remote cache
cache eviction
explainable cache misses
link result cache
per-target cache summaries
native vix.app graph integrationThe key is to build the cache in layers.
Engineering principle
The object cache should follow one principle:
never trade correctness for speedIf Vix cannot prove that a cached object is valid, it should compile.
A slow correct build is acceptable.
A fast wrong build is not.
Conclusion
Object caching is one of the main ways vix build can become significantly faster.
It works by reusing compiled object files when the full compile identity matches.
That identity includes more than the source file.
It includes headers, compiler, flags, defines, include paths, standard, target triple, and build configuration.
Combined with the BuildGraph, Scheduler, and ArtifactCache, the object cache gives Vix a path toward much faster incremental builds.
The long-term direction is:
vix.app
-> BuildGraph
-> ObjectCache
-> Scheduler
-> LinkThat is where Vix can make repeated C++ builds feel dramatically faster without pretending that C++ compilation itself is free.