Appearance
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:
txt
unchanged source files
unchanged headers
unchanged compiler flags
unchanged dependencies
unchanged build configurationThe purpose of an object cache is simple:
txt
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:
txt
src/main.cpp -> main.o
src/app.cpp -> app.oThen the linker combines object files into the final output:
txt
main.o + app.o -> myappSo if only src/app.cpp changed, the ideal incremental build is:
txt
recompile app.cpp
reuse main.o
relink myappIt should not recompile every source file.
The basic incremental build model
A simple project:
txt
myapp/
include/
myapp/
app.hpp
src/
main.cpp
app.cppBuild graph:
txt
src/main.cpp -> main.o
src/app.cpp -> app.o
main.o + app.o -> myappIf src/app.cpp changes:
txt
main.o stays valid
app.o becomes dirty
myapp must relinkA good build system should rebuild only:
txt
app.o
myappnot:
txt
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:
txt
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:
txt
Is the existing object file in the build directory still valid?An object cache asks:
txt
Have I ever built this exact object before?That distinction matters.
If the build directory is removed:
bash
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:
txt
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:
txt
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:
txt
src/app.cppthe object file must be rebuilt.
A source file can be checked through:
txt
timestamp
file size
content hashFor cache keys, content hashes are safer.
Header dependencies
Headers are just as important as source files.
Example:
cpp
#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:
txt
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:
bash
g++ -std=c++20 -Iinclude -c src/app.cpp -o app.ois not the same as:
bash
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:
txt
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:
txt
compiler path
compiler version
target tripleBuild type
Debug and release builds are different.
A debug object may be compiled with:
txt
-g
-O0A release object may be compiled with:
txt
-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:
bash
g++ -Iinclude -c src/app.cppcan produce a different result from:
bash
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:
ini
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.
ini
standard = c++20is not equivalent to:
ini
standard = c++23The C++ standard must be part of the object identity.
Target triple
Cross-compilation changes the output.
A cached object for:
txt
x86_64-linux-gnucannot be reused for:
txt
aarch64-linux-gnuThe target triple must be part of the cache key.
Object cache flow
A compile task can follow this flow:
txt
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:
txt
Vix found a previously compiled object with the same compile identity.Then Vix can copy or restore:
txt
cached object -> build output object
cached dependency file -> build dependency fileThis avoids running the compiler.
Cache miss
A cache miss means:
txt
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:
txt
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:
txt
build-ninja/
main.o
app.oIf app.cpp changes:
txt
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:
txt
~/.vix/cache/objects/or another cache location can store compiled outputs.
Then Vix can restore objects even after:
txt
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:
txt
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:
txt
compile tasks
source inputs
header dependencies
object outputs
link inputs
target outputsThe object cache knows:
txt
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:
txt
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:
txt
src/main.cpp
src/app.cpp
src/server.cppChange:
txt
src/app.cppExpected behavior:
txt
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:
txt
src/main.cpp includes include/app.hpp
src/server.cpp includes include/server.hppChange:
txt
include/app.hppExpected behavior:
txt
main.o -> dirty
server.o -> clean
myapp -> relinkThe dependency file tells Vix which object depends on which header.
Example: define changed
Manifest changes:
ini
defines = [
ENABLE_LOGGING=1,
]to:
ini
defines = [
ENABLE_LOGGING=0,
]Expected behavior:
txt
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:
txt
g++ 13to:
txt
clang++ 18Expected behavior:
txt
object cache keys change
old objects are not reused
project rebuilds safelyThis is correct.
Example: clean build with warm cache
First build:
txt
compile main.cpp
compile app.cpp
link myapp
store objectsThen:
bash
rm -rf build-ninja
vix buildWith warm object cache:
txt
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:
txt
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:
txt
object file
dependency file
metadata
compile identity
source hash
dependency hashes
compiler info
command hash
created timeThe metadata helps Vix explain cache behavior.
Example:
txt
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:
txt
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:
txt
same key means same compile resultor at least close enough to be safe under the build model.
Cache metadata
Metadata can include:
txt
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:
txt
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:
txt
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:
txt
~/.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:
txt
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:
txt
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:
txt
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:
txt
restore object
or compile sourcebefore scheduling link.
Object cache and CMake projects
For arbitrary CMake projects, Vix should not guess.
It should import:
txt
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:
txt
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:
txt
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:
txt
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:
txt
restore or compile objects
then relink if link inputs changedA separate artifact cache can optimize final outputs.
Artifact cache vs object cache
Object cache:
txt
source -> objectArtifact cache:
txt
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:
txt
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:
txt
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:
txt
Why did this file rebuild?Example explanations:
txt
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:
txt
reuse g++ object for clang++ buildCorrect:
txt
compiler identity is part of cache keyIgnoring headers
Wrong:
txt
cache key = source hash onlyCorrect:
txt
cache key includes dependency headersIgnoring compile flags
Wrong:
txt
same source means same objectCorrect:
txt
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:
txt
compile command hash
source hash
dependency file hash
compiler identity
object output
dependency outputFlow:
txt
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:
txt
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:
txt
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:
txt
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.