Skip to content

Commit 6b44117

Browse files
michaelwoeristermark-i-m
authored andcommitted
Update description of incremental compilation.
1 parent ed2ad0e commit 6b44117

File tree

2 files changed

+98
-27
lines changed

2 files changed

+98
-27
lines changed

src/queries/incremental-compilation-in-detail.md

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ simple extension to the overall query system. It relies on the fact that:
1111
This chapter will explain how we can use these properties for making things
1212
incremental and then goes on to discuss version implementation issues.
1313

14-
# A Basic Algorithm For Incremental Query Evaluation
14+
## A Basic Algorithm For Incremental Query Evaluation
1515

1616
As explained in the [query evaluation model primer][query-model], query
1717
invocations form a directed-acyclic graph. Here's the example from the
@@ -36,17 +36,17 @@ we know which queries were invoked (the nodes of the graph) and for each
3636
invocation, which other queries or input has gone into computing the query's
3737
result (the edges of the graph).
3838

39-
Now suppose, we change the source code of our program so that
39+
Now suppose we change the source code of our program so that
4040
HIR of `bar` looks different than before. Our goal is to only recompute
41-
those queries that are actually affected by the change while just re-using
41+
those queries that are actually affected by the change while re-using
4242
the cached results of all the other queries. Given the dependency graph we can
4343
do exactly that. For a given query invocation, the graph tells us exactly
4444
what data has gone into computing its results, we just have to follow the
4545
edges until we reach something that has changed. If we don't encounter
4646
anything that has changed, we know that the query still would evaluate to
4747
the same result we already have in our cache.
4848

49-
Taking the `type_of(foo)` invocation from above as example, we can check
49+
Taking the `type_of(foo)` invocation from above as an example, we can check
5050
whether the cached result is still valid by following the edges to its
5151
inputs. The only edge leads to `Hir(foo)`, an input that has not been affected
5252
by the change. So we know that the cached result for `type_of(foo)` is still
@@ -63,9 +63,9 @@ turn will re-run `type_of(bar)`, which will yield an up-to-date result
6363
because it reads the up-to-date version of `Hir(bar)`.
6464

6565

66-
# The Problem With The Basic Algorithm: False Positives
66+
## The Problem With The Basic Algorithm: False Positives
6767

68-
If you read the previous paragraph carefully, you'll notice that it says that
68+
If you read the previous paragraph carefully you'll notice that it says that
6969
`type_of(bar)` *might* have changed because one of its inputs has changed.
7070
There's also the possibility that it might still yield exactly the same
7171
result *even though* its input has changed. Consider an example with a
@@ -90,15 +90,15 @@ of examples like this and small changes to the input often potentially affect
9090
very large parts of the output binaries. As a consequence, we had to make the
9191
change detection system smarter and more accurate.
9292

93-
# Improving Accuracy: The red-green Algorithm
93+
## Improving Accuracy: The red-green Algorithm
9494

9595
The "false positives" problem can be solved by interleaving change detection
9696
and query re-evaluation. Instead of walking the graph all the way to the
9797
inputs when trying to find out if some cached result is still valid, we can
9898
check if a result has *actually* changed after we were forced to re-evaluate
9999
it.
100100

101-
We call this algorithm, for better or worse, the red-green algorithm because nodes
101+
We call this algorithm the red-green algorithm because nodes
102102
in the dependency graph are assigned the color green if we were able to prove
103103
that its cached result is still valid and the color red if the result has
104104
turned out to be different after re-evaluating it.
@@ -128,7 +128,7 @@ fn try_mark_green(tcx, current_node) -> bool {
128128
return false
129129
}
130130
Unknown => {
131-
// This is the first time we are look at this node. Let's try
131+
// This is the first time we look at this node. Let's try
132132
// to mark it green by calling try_mark_green() recursively.
133133
if try_mark_green(tcx, dependency) {
134134
// We successfully marked the input as green, on to the
@@ -186,15 +186,15 @@ invoke the query provider to re-compute the result.
186186

187187

188188

189-
# The Real World: How Persistence Makes Everything Complicated
189+
## The Real World: How Persistence Makes Everything Complicated
190190

191191
The sections above described the underlying algorithm for incremental
192192
compilation but because the compiler process exits after being finished and
193-
takes the query context with its result cache with it into oblivion, we have
193+
takes the query context with its result cache with it into oblivion, we have to
194194
persist data to disk, so the next compilation session can make use of it.
195195
This comes with a whole new set of implementation challenges:
196196

197-
- The query results cache is stored to disk, so they are not readily available
197+
- The query result cache is stored to disk, so they are not readily available
198198
for change comparison.
199199
- A subsequent compilation session will start off with new version of the code
200200
that has arbitrary changes applied to it. All kinds of IDs and indices that
@@ -205,11 +205,11 @@ This comes with a whole new set of implementation challenges:
205205
- Persisting things to disk comes at a cost, so not every tiny piece of
206206
information should be actually cached in between compilation sessions.
207207
Fixed-sized, plain-old-data is preferred to complex things that need to run
208-
branching code during (de-)serialization.
208+
through an expensive (de-)serialization step.
209209

210210
The following sections describe how the compiler currently solves these issues.
211211

212-
## A Question Of Stability: Bridging The Gap Between Compilation Sessions
212+
### A Question Of Stability: Bridging The Gap Between Compilation Sessions
213213

214214
As noted before, various IDs (like `DefId`) are generated by the compiler in a
215215
way that depends on the contents of the source code being compiled. ID assignment
@@ -253,7 +253,7 @@ the `LocalId`s within it are still the same.
253253

254254

255255

256-
## Checking Query Results For Changes: StableHash And Fingerprints
256+
### Checking Query Results For Changes: HashStable And Fingerprints
257257

258258
In order to do red-green-marking we often need to check if the result of a
259259
query has changed compared to the result it had during the previous
@@ -273,7 +273,7 @@ value of the result. We call this hash value "the `Fingerprint` of the query
273273
result". The hashing is (and has to be) done "in a stable way". This means
274274
that whenever something is hashed that might change in between compilation
275275
sessions (e.g. a `DefId`), we instead hash its stable equivalent
276-
(e.g. the corresponding `DefPath`). That's what the whole `StableHash`
276+
(e.g. the corresponding `DefPath`). That's what the whole `HashStable`
277277
infrastructure is for. This way `Fingerprint`s computed in two
278278
different compilation sessions are still comparable.
279279

@@ -300,12 +300,8 @@ This approach works rather well but it's not without flaws:
300300
use a good and thus expensive hash function, and we have to map things to
301301
their stable equivalents while doing the hashing.
302302

303-
In the future we might want to explore different approaches to this problem.
304-
For now it's `StableHash` and `Fingerprint`.
305303

306-
307-
308-
## A Tale Of Two DepGraphs: The Old And The New
304+
### A Tale Of Two DepGraphs: The Old And The New
309305

310306
The initial description of dependency tracking glosses over a few details
311307
that quickly become a head scratcher when actually trying to implement things.
@@ -327,7 +323,7 @@ the given fingerprint, it means that the query key refers to something that
327323
did not yet exist in the previous session.
328324

329325
So, having found the dep-node in the previous dependency graph, we can look
330-
up its dependencies (also dep-nodes in the previous graph) and continue with
326+
up its dependencies (i.e. also dep-nodes in the previous graph) and continue with
331327
the rest of the try-mark-green algorithm. The next interesting thing happens
332328
when we successfully marked the node as green. At that point we copy the node
333329
and the edges to its dependencies from the old graph into the new graph. We
@@ -343,12 +339,86 @@ new graph is serialized out to disk, alongside the query result cache, and can
343339
act as the previous dep-graph in a subsequent compilation session.
344340

345341

346-
## Didn't You Forget Something?: Cache Promotion
347-
TODO
342+
### Didn't You Forget Something?: Cache Promotion
343+
344+
The system described so far has a somewhat subtle property: If all inputs of a
345+
dep-node are green then the dep-node itself can be marked as green without
346+
computing or loading the corresponding query result. Applying this property
347+
transitively often leads to the situation that some intermediate results are
348+
never actually loaded from disk, as in the following example:
349+
350+
```ignore
351+
input(A) <-- intermediate_query(B) <-- leaf_query(C)
352+
```
353+
354+
The compiler might need the value of `leaf_query(C)` in order to generate some
355+
output artifact. If it can mark `leaf_query(C)` as green, it will load the
356+
result from the on-disk cache. The result of `intermediate_query(B)` is never
357+
loaded though. As a consequence, when the compiler persists the *new* result
358+
cache by writing all in-memory query results to disk, `intermediate_query(B)`
359+
will not be in memory and thus will be missing from the new result cache.
360+
361+
If there subsequently is another compilation session that actually needs the
362+
result of `intermediate_query(B)` it will have to be re-computed even though we
363+
had a perfectly valid result for it in the cache just before.
364+
365+
In order to prevent this from happening, the compiler does something called
366+
"cache promotion": Before emitting the new result cache it will walk all green
367+
dep-nodes and make sure that their query result is loaded into memory. That way
368+
the result cache doesn't unnecessarily shrink again.
369+
370+
371+
372+
# Incremental Compilation and the Compiler Backend
373+
374+
The compiler backend, the part involving LLVM, is using the query system but
375+
it is not implemented in terms of queries itself. As a consequence
376+
it does not automatically partake in dependency tracking. However, the manual
377+
integration with the tracking system is pretty straight-forward. The compiler
378+
simply tracks what queries get invoked when generating the initial LLVM version
379+
of each codegen unit, which results in a dep-node for each of them. In
380+
subsequent compilation sessions it then tries to mark the dep-node for a CGU as
381+
green. If it succeeds it knows that the corresponding object and bitcode files
382+
on disk are still valid. If it doesn't succeed, the entire codegen unit has to
383+
be recompiled.
384+
385+
This is the same approach that is used for regular queries. The main differences
386+
are:
387+
388+
- that we cannot easily compute a fingerprint for LLVM modules (because
389+
they are opaque C++ objects),
390+
391+
- that the logic for dealing with cached values is rather different from
392+
regular queries because here we have bitcode and object files instead of
393+
serialized Rust values in the common result cache file, and
394+
395+
- the operations around LLVM are so expensive in terms of computation time and
396+
memory consumption that we need to have tight control over what is
397+
executed when and what stays in memory for how long.
398+
399+
The query system could probably be extended with general purpose mechanisms to
400+
deal with all of the above but so far that seemed like more trouble than it
401+
would save.
402+
403+
404+
# Shortcomings of the Current System
405+
406+
There are many things that still can be improved.
407+
408+
## Incrementality of on-disk data structures
409+
410+
The current system is not able to update on-disk caches and the dependency graph
411+
in-place. Instead it has to rewrite each file entirely in each compilation
412+
session. The overhead of doing so is a few percent of total compilation time.
413+
414+
## Unnecessary data dependencies
348415

416+
Data structures used as query results could be factored in a way that removes
417+
edges from the dependency graph. Especially "span" information is very volatile,
418+
so including it in query result will increase the chance that that result won't
419+
be reusable. See https://github.com/rust-lang/rust/issues/47389 for more
420+
information.
349421

350-
# The Future: Shortcomings Of The Current System and Possible Solutions
351-
TODO
352422

353423

354424
[query-model]: ./query-evaluation-model-in-detail.html

src/queries/query-evaluation-model-in-detail.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ When the query context is created, it is still empty: No queries have been
7575
executed, no results are cached. But the context already provides access to
7676
"input" data, i.e. pieces of immutable data that were computed before the
7777
context was created and that queries can access to do their computations.
78-
Currently this input data consists mainly of the HIR map and the command-line
78+
Currently this input data consists mainly of the HIR map, upstream crate
79+
metadata, and the command-line
7980
options the compiler was invoked with. In the future, inputs will just consist
8081
of command-line options and a list of source files -- the HIR map will itself
8182
be provided by a query which processes these source files.

0 commit comments

Comments
 (0)