Add -Zdead-fn-elimination: skip codegen of functions unreachable from the entry point#158220
Add -Zdead-fn-elimination: skip codegen of functions unreachable from the entry point#158220yijunyu wants to merge 6 commits into
Conversation
|
Thanks for the pull request, and welcome! The Rust Project is excited to review your changes, and you should hear from @oli-obk (or someone else) some time within the next two weeks. Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (
|
This comment has been minimized.
This comment has been minimized.
… the entry point Adds an unstable flag that, for binary crates, computes the set of functions reachable from the entry point (via a MIR call-graph BFS seeded with the reachability set, the entry fn, and vtable-constructed trait methods) and excludes the rest from codegen by filtering them out of the mono item set before partitioning. The analysis is conservative: it never eliminates a function in the collected mono set, lang items, exported symbols, the entry fn, drop glue, generics, or async fns. A debug-assertions invariant checks reachable_set is a subset of the post-BFS set. The flag is inert when unset.
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
I still don't get what you are trying to do. If functions are unreachable from monomorphization roots (
Can you show a list of functions that are actually eliminated? |
|
You're right, and thanks for pushing on this — I checked, and on a binary the analysis as written eliminates nothing the collector would have emitted. I instrumented it to print, for every function the pass marks eliminable, whether the monomorphization collector had actually collected it. On ripgrep: All 91 are items the lazy collector never collected in the first place — uninstantiated derive impls and helpers, e.g.: So the The only place this analysis removes anything the collector does emit is cross-crate: a library is compiled before its callers, so its collector must conservatively emit every Given that, I think the useful question is whether the cross-crate version is something the team would want to pursue. The mechanism in this PR (the codegen-skipping hook + the pre-partition filter) is the plumbing a seed-consuming flag would need, but on its own — without a seed source telling a library which of its A couple of directions I could take it:
I'm happy to go whichever way the team thinks is most useful — including parking this until the cross-crate design is worked out. What would you prefer? |
The binary-only pass is a no-op: a binary's collector is already exact. The real slack is cross-crate — a library emits its whole `pub` closure, most of which no final binary calls. This adds `-Zdead-fn-used-set=<file>`: a library recompilation consumes a used-set (the mangled symbols a downstream binary links from this crate, e.g. from `nm`) and drops the `pub` functions absent from it before partition(). - new module used_set.rs: parse the used-set, match by mangled symbol name (link-truth, the same identity nm reports for the final binary) - run_analysis: seed the BFS from the used-set for library crates; a pub fn reachable only by visibility becomes a candidate when the binary never links it - the local-collector keep-guard is skipped in the cross-crate case (a library eagerly collects its whole pub closure — that is the over-emission removed here); soundness is still enforced by is_locally_safe_to_eliminate (linker-visible / lang / vtable / drop-glue / generics are never dropped) Verified end-to-end on stage1: a 3-pub-fn dependency, binary using 1, drops the 2 unused fns from codegen; binary still links and runs correctly.
…nation The nm-based producer needed the binary linked first (full codegen of every dep), so it could only help a *second* build. This adds the real first-build mechanism: -Zdead-fn-emit-used-set=<dir>: walk the binary's MIR in the analysis pass (post-analysis, BEFORE codegen — works on --emit=metadata) and record every extern non-generic fn it calls or references. Written as per-dependency used-set files (keyed by mangled symbol name). The loop becomes: pass 1 = metadata-only + probe (no codegen); pass 2 = codegen each dependency ONCE, pruned against its used-set. Dead pub fns are never codegen'd in the first place. Measured (200-fn broad-API dep, binary uses 1): plain full build 0.30s vs probe(0.04s)+pruned(0.13s)=0.17s, 43% faster; 200 of 201 fns never codegen'd; binary still links and runs. The probe is per-binary (amortized over all deps), the pruned codegen per-dep, so for N deps: plain=N*full, xdce=probe+N*pruned.
For a first-build speedup the used-set must come from the binary's MIR (no codegen) and each dependency codegen'd once, pruned. A dependency's frontend must run once — not re-run in a second process. This flag lets a library emit its metadata (unblocking downstream via pipelining), then block in the *same process* for up to N seconds until the used-set file appears, then codegen pruned. The rustc side is now complete for the first-build path. It requires Cargo to schedule the binary's frontend (which produces the used-set) during the library's wait — bin-frontend -> dep-codegen -> bin-link. A RUSTC_WRAPPER cannot reorder Cargo's job graph, so end-to-end integration is Cargo-side work; see the design doc's three options (split binary unit / whole-program / deferred codegen).
This comment has been minimized.
This comment has been minimized.
The module comment still described the discarded nm/link-truth producer keyed on mangled symbol names; the code emits the used-set from the binary's MIR (pre-codegen) keyed on DefPathHash local_hash. Rewrite the doc to describe the implemented design so code and comment agree.
The used-set of a dependency is the union over every crate that calls it: in Main->A->B, B::used is referenced from A's body, not main's, so only A's probe names it. Each crate's probe therefore appends its extern references to the dependency's used-set file rather than overwriting; the consumer dedups on load. Verified on Main/A/B: B's used-set is complete (1 entry), 2000 dead fns pruned.
|
The job Click to see the possible cause of the failure (guessed by this bot) |
Nice LLM-ism. I'm guessing your PR description, response, and code is AI generated? |
Implements the accepted MCP compiler-team#976: an unstable flag that skips codegen of functions a binary cannot reach from its entry point.
What it does
-Zdead-fn-elimination(off by default) adds, for binary crates only, a reachability analysis that runs after monomorphization collection and before partitioning. Functions unreachable from the entry point are dropped from the mono item set, so the backend never generates LLVM IR for them.The flag is inert when unset.
How it works
In
collect_and_partition_mono_items, aftercollect_crate_mono_itemsand beforepartition:optimized_mir), recording direct calls, function references in constants/statics, address-taken functions, and inline-asmsymoperands.reachable_set(covers exported/pub/lang-item/#[no_mangle]/#[used]items), the entry fn, and the methods of anydyn-constructed trait (dynamic dispatch is invisible to a call-graph walk, so vtable methods are seeded explicitly).retainthe mono item set, dropping only localMonoItem::Fns that are eliminable.Conservatism
The analysis never eliminates:
A
#[cfg(debug_assertions)]invariant checks thatreachable_setis a subset of the post-BFS reachable set.Soundness testing
Verified on a stage1 build by compiling real binaries with and without the flag using the same compiler and diffing the output:
-n/-i/-c/-w/-v/-o,--json, recursive)math sum,strops,sort, record access,eachclosures)Edge cases checked: flag-off and flag-on both produce reproducible binaries; library crates are inert (no entry seed); the
#[test]harness still runs;panic=abortworks;-C link-dead-codecorrectly defers (no elimination). Functions reached only through a closure (Iterator::map), a trait object (Box<dyn Fn>), or a function pointer are kept — only genuinely-unreachable functions are removed.tests/codegen-units(46) andtests/ui/codegen(147) pass; tidy is clean.Scope
This is the in-rustc flag only. It is self-contained and reviewable in isolation; there is no build-system or cross-crate coordination in this PR.
r? @oli-obk