Skip to content

Commit 01bb480

Browse files
committed
Add project goal proposal for Relink dont Rebuild
1 parent 0d0f95d commit 01bb480

File tree

4 files changed

+3560
-0
lines changed

4 files changed

+3560
-0
lines changed

src/2025h2/relink-dont-rebuild.md

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Relink don't Rebuild
2+
3+
4+
| Metadata | |
5+
| -------- | --- |
6+
| Point of contact | @yaahc |
7+
| Teams | @compiler, @cargo |
8+
| Task Owner(s) | @yaahc, @dropbear32 |
9+
| Status | Proposed |
10+
| Tracking Issue | TBD |
11+
| Zulip Channel | TBD |
12+
13+
## Summary
14+
15+
Work towards avoiding rebuilds of a crate's dependents for changes that don't affect the crate's
16+
public interface.
17+
18+
<!--Our end goal is to have some flags available on nightly that enable avoiding unnecessary rebuilds in certain cases. -->
19+
20+
<!--
21+
Links:
22+
- https://github.com/rust-lang/compiler-team/issues/790
23+
- Piotr's cargo issue: https://github.com/rust-lang/cargo/issues/14604
24+
+ branch: https://github.com/rust-lang/cargo/compare/master...osiewicz:cargo:api-fingerprinting
25+
- Piotr's rustc branch: https://github.com/rust-lang/rust/compare/master...osiewicz:rust:api-fingerprinting
26+
-->
27+
28+
## Motivation
29+
30+
<!--*Begin with a few sentences summarizing the problem you are attacking and why it is important.*-->
31+
32+
Changing a comment, reordering use statements, adding a `dbg!` statement to a non-inlinable
33+
function, formatting code, or moving item definitions from one impl block to another
34+
identical one all cause rebuilds of reverse dependencies of that crate.
35+
36+
This clashes with users' intuition for what needs to be rebuilt when certain changes are made
37+
and makes iterating more painful.
38+
39+
As a point of reference, in C and C++ – where there is a strict separation between interface
40+
and implementation in the form of header files – equivalent changes would only cause a
41+
rebuild of the translation unit whose source has been modified. For other units, existing
42+
compiler outputs would be reused (and re-linked into the final binary).
43+
44+
Our goal is to work towards making `cargo` and `rustc` smarter about when they do or don't need to
45+
rebuild dependent crates (reverse dependencies).
46+
47+
### The status quo
48+
49+
<!--*
50+
Elaborate in more detail about the problem you are trying to solve. This section is making
51+
the case for why this particular problem is worth prioritizing with project bandwidth. A
52+
strong status quo section will (a) identify the target audience and (b) give specifics about
53+
the problems they are facing today.
54+
55+
Sometimes it may be useful to start sketching out how you think those problems will be
56+
addressed by your change, as well, though it's not necessary.
57+
*-->
58+
59+
As an example, consider the [`rg` binary in the `ripgrep` package][rg].
60+
61+
[rg]: https://github.com/BurntSushi/ripgrep/blob/3b7fd442a6f3aa73f650e763d7cbb902c03d700e/Cargo.toml
62+
63+
Its crate dependency graph (narrowed to only include dependents of `globset`, a particular
64+
crate in `ripgrep`'s Cargo workspace) looks like this:
65+
```
66+
❯ cargo tree --invert globset
67+
globset v0.4.16
68+
├── grep-cli v0.1.11
69+
│ └── grep v0.3.2
70+
│ └── ripgrep v14.1.1
71+
└── ignore v0.4.23
72+
└── ripgrep v14.1.1
73+
```
74+
75+
```mermaid
76+
flowchart TB
77+
globset
78+
grep-cli
79+
grep
80+
ignore
81+
ripgrep
82+
83+
globset --> ignore --> ripgrep
84+
85+
globset --> grep-cli --> grep --> ripgrep
86+
```
87+
88+
Consider a change that does not alter the interface of the `globset` crate (for example,
89+
modifying a private item or changing a comment within `globset`'s source code).
90+
91+
Here is the output of `cargo build --timings` for an incremental build of `ripgrep` where only
92+
such a change was made to `globset`:
93+
94+
![](https://hackmd.io/_uploads/Hk6AMAB8ge.png)
95+
96+
<!-- TODO: why are rmeta output times not shown?? -->
97+
98+
In the above we see `globset` and all transitive "upstream" dependent crates (up to `ripgrep`)
99+
being rebuilt.
100+
101+
_Ideally_, in this scenario, the transitive dependents of `globset` (that only depend on
102+
`globset`'s "interface") would _not_ need to be rebuilt. This would allow us to skip the
103+
`grep-cli`, `ignore`, `grep`, and `ripgrep` re-compiles and only redo linking of the final
104+
binary ("relink, don't rebuild")[^caveat_linking].
105+
106+
[^caveat_linking]: `cargo --timings` output does not currently differentiate between time spent compiling (i.e. producing the `rlib` for) and linking the final binary (`rg`); the `rg` bar covers time spent for both
107+
108+
For smaller/shallow dep graphs (like the above) the extra rebuilds are tolerable, but for deeper
109+
graphs, these rebuilds significantly impact edit-debug cycle times.
110+
111+
<!-- maybe: TODO: show numbers from piotr's measurements (or grab our own fresh ones using his branch and specify which versions we're benchmarking) as an example of the upper bound
112+
-->
113+
114+
<!--
115+
From original MCP: https://github.com/rust-lang/compiler-team/issues/790
116+
117+
> Over at Zed we've noticed that even seemingly innocent changes to the crate that has many
118+
> dependents causes us to rebuild (almost) the whole world. Adding a `dbg!` statement to the
119+
> body of non-inlineable function, formatting code, updating a dependency to a new minor
120+
> version and such still forces us to rebuild all dependants of an affected crate.
121+
-->
122+
123+
---
124+
125+
#### Transitive Deps and the Build System View
126+
127+
Ideally the crate-level dependency graph above would (morally) correspond to a build graph like
128+
this[^caveat_pipelining]:
129+
130+
```mermaid
131+
flowchart TB
132+
subgraph globset[globset compile]
133+
globset.rmeta:::rmeta
134+
globset.rlib:::rlib
135+
end
136+
subgraph grep-cli[grep-cli compile]
137+
grep-cli.rmeta:::rmeta
138+
grep-cli.rlib:::rlib
139+
end
140+
subgraph grep[grep compile]
141+
grep.rmeta:::rmeta
142+
grep.rlib:::rlib
143+
end
144+
subgraph ignore[ignore compile]
145+
ignore.rmeta:::rmeta
146+
ignore.rlib:::rlib
147+
end
148+
subgraph ripgrep[ripgrep compile]
149+
%% ripgrep.rmeta:::rmeta
150+
ripgrep.rlib:::rlib
151+
end
152+
153+
ripgrep_bin["`rg (bin)`"]
154+
155+
classDef rmeta fill:#ea76cb
156+
classDef rlib fill:#2e96f5
157+
158+
%% linker inputs (`rlib`s):
159+
globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin
160+
161+
%% direct deps (`rmeta`s):
162+
globset.rmeta --> ignore
163+
ignore.rmeta --> ripgrep
164+
165+
globset.rmeta --> grep-cli
166+
grep-cli.rmeta --> grep
167+
grep.rmeta --> ripgrep
168+
```
169+
170+
[^caveat_pipelining]:
171+
We have taken some liberties in the above graph w.r.t. pipelining.
172+
Today, `cargo` preforms a single `rustc` invocation to produce the `rlib` and `rmeta`
173+
for each crate – `rmeta` is modeled as an "early out".
174+
Additionally, producing `ripgrep.rlib` and linking (the `rg (bin)` node) happens as part
175+
of a single `rustc` invocation.
176+
177+
In particular, note that crate compiles use the `rmeta`s of their direct dependencies.
178+
179+
However, in reality crate compiles need access to all _transitive_ `rmeta`s:
180+
```mermaid
181+
flowchart TB
182+
subgraph globset[globset compile]
183+
globset.rmeta:::rmeta
184+
globset.rlib:::rlib
185+
end
186+
subgraph grep-cli[grep-cli compile]
187+
grep-cli.rmeta:::rmeta
188+
grep-cli.rlib:::rlib
189+
end
190+
subgraph grep[grep compile]
191+
grep.rmeta:::rmeta
192+
grep.rlib:::rlib
193+
end
194+
subgraph ignore[ignore compile]
195+
ignore.rmeta:::rmeta
196+
ignore.rlib:::rlib
197+
end
198+
subgraph ripgrep[ripgrep compile]
199+
%% ripgrep.rmeta:::rmeta
200+
ripgrep.rlib:::rlib
201+
end
202+
203+
ripgrep_bin["`rg (bin)`"]
204+
205+
classDef rmeta fill:#ea76cb
206+
classDef rlib fill:#2e96f5
207+
208+
%% linker inputs (`rlib`s):
209+
globset.rlib & grep-cli.rlib & grep.rlib & ignore.rlib & ripgrep.rlib -.-> ripgrep_bin
210+
211+
%% direct deps (`rmeta`s):
212+
globset.rmeta --> ignore
213+
ignore.rmeta --> ripgrep
214+
215+
globset.rmeta --> grep-cli
216+
grep-cli.rmeta --> grep
217+
grep.rmeta --> ripgrep
218+
219+
%% transitive deps (`rmeta`s):
220+
globset.rmeta ==> ripgrep & grep
221+
grep-cli.rmeta ==> ripgrep
222+
```
223+
224+
This means that when a crate's `rmeta` changes, the `rustc` invocations corresponding to all
225+
transitive dependents of that crate are rerun (even if intermediate `rmeta`s are the
226+
same).
227+
228+
More concretely: when `globset.rmeta` changes, `grep` is rebuilt – even if `grep-cli.rmeta`
229+
(after `grep-cli` is re-compiled) hasn't changed.
230+
231+
The fact that crate compiles depend on the `rmeta`s for all transitive dependencies is
232+
significant because it inhibits our ability to get "early cutoff" (ECO). In reality, crates
233+
compiles are only _actually_ sensitive to the subset of their transitive deps exposed via
234+
their direct deps but under this view (file-level, in the eyes of the build system) crates are
235+
sensitive to transitive dependencies in their entirety.
236+
237+
More concretely: the `grep` crate is only sensitive to the parts of `globset` accessible via
238+
`grep_cli` – if a change is made to `globset` that doesn't affect this subset, we'd expect to
239+
see `grep_cli` being rebuilt but the existing `grep` outputs being reused (no `grep` rebuild).
240+
241+
> [!NOTE]
242+
> ["Early cutoff" (ECO)][bsac] refers to a build system optimization where we are able to detect
243+
> that a freshly-built artifact is identical to a prior one and to then reuse existing
244+
> artifacts of dependent crates from then on (instead of continuing to rebuild them).
245+
246+
[bsac]: https://www.microsoft.com/en-us/research/wp-content/uploads/2020/04/build-systems-jfp.pdf
247+
248+
<!-- ----
249+
250+
Today, many kinds of changes that are "morally" private result in downstream crates being
251+
recompiled.
252+
253+
For the example from above: consider a "no-op" edit or an edit to private function in the
254+
`globset` crate. This does not change the "interface"[^interface] for `globset`.
255+
256+
Ideally, the build system would be aware of this and would be able to skip rebuilding
257+
"upstream" crates that only depend on `globset`'s interface.
258+
259+
> [!NOTE]
260+
> This ability – to detect that a freshly-built artifact is identical to the prior one
261+
> and to reuse existing artifacts of dependent crates from then on (instead of continuing
262+
> to rebuild downstream) – is called ["early cutoff" ("ECO")][bsac].
263+
264+
[bsac]: https://www.microsoft.com/en-us/research/wp-content/uploads/2020/04/build-systems-jfp.pdf
265+
266+
NOTE: From a build system perspective, crate compilations are _currently_ not only
267+
sensitive to the interface outputs of direct deps (todo: graph edge color) but also to
268+
those of all _transitive_ dependencies (todo: graph edge color). This means that changes
269+
to the interface output of crates deep within a graph necessitates rebuilds of _all
270+
transitive reverse dependencies_ (i.e. in the above, `rustc_TBD`'s interface output
271+
changing means `rustc_main` definitely will rebuild – even if `rustc_...`'s interface
272+
output (after rebuilding) remained the same).
273+
274+
Ideally this would not be the case; dependents should only be sensitive to the _subset_
275+
of a crate that is available to them via direct dependencies (i.e. in the above, `rust_...`
276+
should only be rebuilt if the change to `rustc_TBD` affected the subset that's involved
277+
in `rustc_TBD`'s interface).
278+
279+
- (TODO: mention spans also, point to Jane's new unstable flag)
280+
- https://github.com/rust-lang/rust/pull/143249
281+
-->
282+
### The next 6 months
283+
284+
<!--*Sketch out the specific things you are trying to achieve in this goal period. This should be short and high-level -- we don't want to see the design!*-->
285+
286+
* Identify and remove "oversensitivity" in `.rmeta`
287+
- i.e. changes to spans, comments, etc. will not affect the `.rmeta`
288+
- coupled with cargo's unstable [`checksum-freshness` feature](https://github.com/rust-lang/cargo/issues/14136),
289+
this would avoid triggering rebuilds for dependent crates
290+
* Make `DefId`s more stable when items are added or reordered
291+
- today this is a major source of differences in compiler output
292+
- there are other things like `SymbolIndex`es which we may also want to stabilize
293+
* Work on designs for enabling "transitive" ECO
294+
- i.e. the decision to rebuild should factor in what parts of a transitive crate dep are
295+
accessible via direct deps
296+
297+
### The "shiny future" we are working towards
298+
299+
<!-- *If this goal is part of a larger plan that will extend beyond this goal period, sketch out the goal you are working towards. It may be worth adding some text about why these particular goals were chosen as the next logical step to focus on.* >
300+
301+
*This text is NORMATIVE, in the sense that teams should review this and make sure they are aligned. If not, then the shiny future should be moved to frequently asked questions with a title like "what might we do next".*-->
302+
303+
Only changes to a crate that affect the public interface of the crate should cause downstream
304+
crates to rebuild.
305+
306+
<!--
307+
## Design axioms
308+
309+
(maybe don't include this section given that we're not settled on an approach/deliverables)
310+
311+
- go after the common case (change private function body/change a comment/add private item)
312+
- incremental improvements? (i.e. cover more cases over time)
313+
- correctness/testability (i mean... imo; i think we all feel differently about this) ((try to be as-or-more correct than rustc-incremental))
314+
-->
315+
316+
## Ownership and team asks
317+
318+
<!--
319+
**Owner:** *Identify a specific person or small group of people if possible, else the group that will provide the owner. Github user names are commonly used to remove ambiguity.*
320+
321+
*This section defines the specific work items that are planned and who is expected to do them. It should also include what will be needed from Rust teams. The table below shows some common sets of asks and work, but feel free to adjust it as needed. Every row in the table should either correspond to something done by a contributor or something asked of a team. For items done by a contributor, list the contributor, or ![Heap wanted][] if you don't yet know who will do it. For things asked of teams, list ![Team][] and the name of the team. The things typically asked of teams are defined in the [Definitions](#definitions) section below.* -->
322+
323+
| Subgoal | Owner(s) or team(s) | Notes |
324+
| ----------------------------- | ----------------------- | ----- |
325+
| Design meeting | ![Team][] [compiler] | |
326+
| Discussion and moral support | ![Team][] [compiler] ![Team][] [cargo] | |
327+
| Nightly experiment for RDR | | |
328+
| ↳ Author MCP | @osiewicz | [already accepted](https://github.com/rust-lang/compiler-team/issues/790) |
329+
| ↳ Rustc Implementation | | [WIP](https://github.com/osiewicz/rust/tree/api-fingerprinting) |
330+
| ↳ Cargo Implementation | | [WIP](https://github.com/osiewicz/cargo/tree/api-fingerprinting) |
331+
| ↳ Standard reviews | ![Team][] [compiler] ![Team][] [cargo] | |
332+
333+
### Definitions
334+
335+
Definitions for terms used above:
336+
337+
* *Discussion and moral support* is the lowest level offering, basically committing the team to nothing but good vibes and general support for this endeavor.
338+
* *Design meeting* means holding a synchronous meeting to review a proposal and provide feedback (no decision expected).
339+
* *Standard reviews* refers to reviews for PRs against the repository; these PRs are not expected to be unduly large or complicated.
340+
* Other kinds of decisions:
341+
* Compiler [Major Change Proposal (MCP)](https://forge.rust-lang.org/compiler/mcp.html) is used to propose a 'larger than average' change and get feedback from the compiler team.
342+
343+
## Frequently asked questions
344+
345+
### Isn't `rustc` incremental enough?
346+
347+
Theoretically, yes: under a system like `rust-analyzer` where there isn't chunking of work along
348+
crate/file/process invocation boundaries, incremental compilation would obviate this effort.
349+
350+
However under `rustc`'s current achitecture (1 process invocation per crate, new process
351+
invocation for each compile rather than a daemon): RDR (i.e. being able to skip `rustc`
352+
invocations) still matters.
353+
354+
Right now even when 100% of a compile's incremental queries hit the cache (such as when you
355+
`touch` a source file; i.e. [`incr-unchanged`](https://perf.rust-lang.org/)) it still takes
356+
non-negligible amounts of time to replay those queries and re-emit compiler outputs (see
357+
[zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/rmeta.20stability/near/501691783)).
358+
359+
<!--
360+
todo: why not devote resources towards just speeding up `rustc` incrmental?
361+
- hard? invasive?
362+
- best case scenario still doesn't obviate the upside that RDR-esque work reuse provides? (not sure if this is actually the case... can you make incremental fast enough that we don't care anymore?)
363+
- (there used to be a pure build system angle in here but... there's not anymore)
364+
365+
### pure build systems/build systems other than cargo?
366+
367+
idk that we want to draw attention to this or that it's worth calling out...
368+
-->
369+
370+
###
371+
372+
<!--
373+
374+
### What do I do with this space?
375+
376+
This is a good place to elaborate on your reasoning above -- for example, why did you put the
377+
design axioms in the order that you did? It's also a good place to put the answers to any
378+
questions that come up during discussion. The expectation is that this FAQ section will grow
379+
as the goal is discussed and eventually should contain a complete summary of the points
380+
raised along the way.
381+
382+
-->
383+

0 commit comments

Comments
 (0)