@@ -11,7 +11,7 @@ simple extension to the overall query system. It relies on the fact that:
11
11
This chapter will explain how we can use these properties for making things
12
12
incremental and then goes on to discuss version implementation issues.
13
13
14
- # A Basic Algorithm For Incremental Query Evaluation
14
+ ## A Basic Algorithm For Incremental Query Evaluation
15
15
16
16
As explained in the [ query evaluation model primer] [ query-model ] , query
17
17
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
36
36
invocation, which other queries or input has gone into computing the query's
37
37
result (the edges of the graph).
38
38
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
40
40
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
42
42
the cached results of all the other queries. Given the dependency graph we can
43
43
do exactly that. For a given query invocation, the graph tells us exactly
44
44
what data has gone into computing its results, we just have to follow the
45
45
edges until we reach something that has changed. If we don't encounter
46
46
anything that has changed, we know that the query still would evaluate to
47
47
the same result we already have in our cache.
48
48
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
50
50
whether the cached result is still valid by following the edges to its
51
51
inputs. The only edge leads to ` Hir(foo) ` , an input that has not been affected
52
52
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
63
63
because it reads the up-to-date version of ` Hir(bar) ` .
64
64
65
65
66
- # The Problem With The Basic Algorithm: False Positives
66
+ ## The Problem With The Basic Algorithm: False Positives
67
67
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
69
69
` type_of(bar) ` * might* have changed because one of its inputs has changed.
70
70
There's also the possibility that it might still yield exactly the same
71
71
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
90
90
very large parts of the output binaries. As a consequence, we had to make the
91
91
change detection system smarter and more accurate.
92
92
93
- # Improving Accuracy: The red-green Algorithm
93
+ ## Improving Accuracy: The red-green Algorithm
94
94
95
95
The "false positives" problem can be solved by interleaving change detection
96
96
and query re-evaluation. Instead of walking the graph all the way to the
97
97
inputs when trying to find out if some cached result is still valid, we can
98
98
check if a result has * actually* changed after we were forced to re-evaluate
99
99
it.
100
100
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
102
102
in the dependency graph are assigned the color green if we were able to prove
103
103
that its cached result is still valid and the color red if the result has
104
104
turned out to be different after re-evaluating it.
@@ -128,7 +128,7 @@ fn try_mark_green(tcx, current_node) -> bool {
128
128
return false
129
129
}
130
130
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
132
132
// to mark it green by calling try_mark_green() recursively.
133
133
if try_mark_green(tcx, dependency) {
134
134
// We successfully marked the input as green, on to the
@@ -186,15 +186,15 @@ invoke the query provider to re-compute the result.
186
186
187
187
188
188
189
- # The Real World: How Persistence Makes Everything Complicated
189
+ ## The Real World: How Persistence Makes Everything Complicated
190
190
191
191
The sections above described the underlying algorithm for incremental
192
192
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
194
194
persist data to disk, so the next compilation session can make use of it.
195
195
This comes with a whole new set of implementation challenges:
196
196
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
198
198
for change comparison.
199
199
- A subsequent compilation session will start off with new version of the code
200
200
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:
205
205
- Persisting things to disk comes at a cost, so not every tiny piece of
206
206
information should be actually cached in between compilation sessions.
207
207
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 .
209
209
210
210
The following sections describe how the compiler currently solves these issues.
211
211
212
- ## A Question Of Stability: Bridging The Gap Between Compilation Sessions
212
+ ### A Question Of Stability: Bridging The Gap Between Compilation Sessions
213
213
214
214
As noted before, various IDs (like ` DefId ` ) are generated by the compiler in a
215
215
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.
253
253
254
254
255
255
256
- ## Checking Query Results For Changes: StableHash And Fingerprints
256
+ ### Checking Query Results For Changes: HashStable And Fingerprints
257
257
258
258
In order to do red-green-marking we often need to check if the result of a
259
259
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
273
273
result". The hashing is (and has to be) done "in a stable way". This means
274
274
that whenever something is hashed that might change in between compilation
275
275
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 `
277
277
infrastructure is for. This way ` Fingerprint ` s computed in two
278
278
different compilation sessions are still comparable.
279
279
@@ -300,12 +300,8 @@ This approach works rather well but it's not without flaws:
300
300
use a good and thus expensive hash function, and we have to map things to
301
301
their stable equivalents while doing the hashing.
302
302
303
- In the future we might want to explore different approaches to this problem.
304
- For now it's ` StableHash ` and ` Fingerprint ` .
305
303
306
-
307
-
308
- ## A Tale Of Two DepGraphs: The Old And The New
304
+ ### A Tale Of Two DepGraphs: The Old And The New
309
305
310
306
The initial description of dependency tracking glosses over a few details
311
307
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
327
323
did not yet exist in the previous session.
328
324
329
325
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
331
327
the rest of the try-mark-green algorithm. The next interesting thing happens
332
328
when we successfully marked the node as green. At that point we copy the node
333
329
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
343
339
act as the previous dep-graph in a subsequent compilation session.
344
340
345
341
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
348
415
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.
349
421
350
- # The Future: Shortcomings Of The Current System and Possible Solutions
351
- TODO
352
422
353
423
354
424
[ query-model ] : ./query-evaluation-model-in-detail.html
0 commit comments