-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto merge of #67020 - pnkfelix:issue-59535-accumulate-past-lto-impor…
…ts, r=mw save LTO import info and check it when trying to reuse build products Fix #59535 Previous runs of LTO optimization on the previous incremental build can import larger portions of the dependence graph into a codegen unit than the current compilation run is choosing to import. We need to take that into account when we choose to reuse PostLTO-optimization object files from previous compiler invocations. This PR accomplishes that by serializing the LTO import information on each incremental build. We load up the previous LTO import data as well as the current LTO import data. Then as we decide whether to reuse previous PostLTO objects or redo LTO optimization, we check whether the LTO import data matches. After we finish with this decision process for every object, we write the LTO import data back to disk. ---- What is the scenario where comparing against past LTO import information is necessary? I've tried to capture it in the comments in the regression test, but here's yet another attempt from me to summarize the situation: 1. Consider a call-graph like `[A] -> [B -> D] <- [C]` (where the letters are functions and the modules are enclosed in `[]`) 2. In our specific instance, the earlier compilations were inlining the call to`B` into `A`; thus `A` ended up with a external reference to the symbol `D` in its object code, to be resolved at subsequent link time. The LTO import information provided by LLVM for those runs reflected that information: it explicitly says during those runs, `B` definition and `D` declaration were imported into `[A]`. 3. The change between incremental builds was that the call `D <- C` was removed. 4. That change, coupled with other decisions within `rustc`, made the compiler decide to make `D` an internal symbol (since it was no longer accessed from other codegen units, this makes sense locally). And then the definition of `D` was inlined into `B` and `D` itself was eliminated entirely. 5. The current LTO import information reported that `B` alone is imported into `[A]` for the *current compilation*. So when the Rust compiler surveyed the dependence graph, it determined that nothing `[A]` imports changed since the last build (and `[A]` itself has not changed either), so it chooses to reuse the object code generated during the previous compilation. 6. But that previous object code has an unresolved reference to `D`, and that causes a link time failure! ---- The interesting thing is that its quite hard to actually observe the above scenario arising, which is probably why no one has noticed this bug in the year or so since incremental LTO support landed (PR #53673). I've literally spent days trying to observe the bug on my local machine, but haven't managed to find the magic combination of factors to get LLVM and `rustc` to do just the right set of the inlining and `internal`-reclassification choices that cause this particular problem to arise. ---- Also, I have tried to be careful about injecting new bugs with this PR. Specifically, I was/am worried that we could get into a scenario where overwriting the current LTO import data with past LTO import data would cause us to "forget" a current import. ~~To guard against this, the PR as currently written always asserts, at overwrite time, that the past LTO import-set is a *superset* of the current LTO import-set. This way, the overwriting process should always be safe to run.~~ * The previous note was written based on the first version of this PR. It has since been revised to use a simpler strategy, where we never attempt to merge the past LTO import information into the current one. We just *compare* them, and act accordingly. * Also, as you can see from the comments on the PR itself, I was quite right to be worried about forgetting past imports; that scenario was observable via a trivial transformation of the regression test I had devised.
- Loading branch information
Showing
3 changed files
with
250 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
src/test/incremental/thinlto/cgu_invalidated_when_import_added.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// revisions: cfail1 cfail2 | ||
// compile-flags: -O -Zhuman-readable-cgu-names -Cllvm-args=-import-instr-limit=10 | ||
// build-pass | ||
|
||
// rust-lang/rust#59535: | ||
// | ||
// This is analgous to cgu_invalidated_when_import_removed.rs, but it covers | ||
// the other direction: | ||
// | ||
// We start with a call-graph like `[A] -> [B -> D] [C]` (where the letters are | ||
// functions and the modules are enclosed in `[]`), and add a new call `D <- C`, | ||
// yielding the new call-graph: `[A] -> [B -> D] <- [C]` | ||
// | ||
// The effect of this is that the compiler previously classfied `D` as internal | ||
// and the import-set of `[A]` to be just `B`. But after adding the `D <- C` call, | ||
// `D` is no longer classified as internal, and the import-set of `[A]` becomes | ||
// both `B` and `D`. | ||
// | ||
// We check this case because an early proposed pull request included an | ||
// assertion that the import-sets monotonically decreased over time, a claim | ||
// which this test case proves to be false. | ||
|
||
fn main() { | ||
foo::foo(); | ||
bar::baz(); | ||
} | ||
|
||
mod foo { | ||
|
||
// In cfail1, ThinLTO decides that foo() does not get inlined into main, and | ||
// instead bar() gets inlined into foo(). | ||
// In cfail2, foo() gets inlined into main. | ||
pub fn foo(){ | ||
bar() | ||
} | ||
|
||
// This function needs to be big so that it does not get inlined by ThinLTO | ||
// but *does* get inlined into foo() when it is declared `internal` in | ||
// cfail1 (alone). | ||
pub fn bar(){ | ||
println!("quux1"); | ||
println!("quux2"); | ||
println!("quux3"); | ||
println!("quux4"); | ||
println!("quux5"); | ||
println!("quux6"); | ||
println!("quux7"); | ||
println!("quux8"); | ||
println!("quux9"); | ||
} | ||
} | ||
|
||
mod bar { | ||
|
||
#[inline(never)] | ||
pub fn baz() { | ||
#[cfg(cfail2)] | ||
{ | ||
crate::foo::bar(); | ||
} | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
src/test/incremental/thinlto/cgu_invalidated_when_import_removed.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// revisions: cfail1 cfail2 | ||
// compile-flags: -O -Zhuman-readable-cgu-names -Cllvm-args=-import-instr-limit=10 | ||
// build-pass | ||
|
||
// rust-lang/rust#59535: | ||
// | ||
// Consider a call-graph like `[A] -> [B -> D] <- [C]` (where the letters are | ||
// functions and the modules are enclosed in `[]`) | ||
// | ||
// In our specific instance, the earlier compilations were inlining the call | ||
// to`B` into `A`; thus `A` ended up with a external reference to the symbol `D` | ||
// in its object code, to be resolved at subsequent link time. The LTO import | ||
// information provided by LLVM for those runs reflected that information: it | ||
// explicitly says during those runs, `B` definition and `D` declaration were | ||
// imported into `[A]`. | ||
// | ||
// The change between incremental builds was that the call `D <- C` was removed. | ||
// | ||
// That change, coupled with other decisions within `rustc`, made the compiler | ||
// decide to make `D` an internal symbol (since it was no longer accessed from | ||
// other codegen units, this makes sense locally). And then the definition of | ||
// `D` was inlined into `B` and `D` itself was eliminated entirely. | ||
// | ||
// The current LTO import information reported that `B` alone is imported into | ||
// `[A]` for the *current compilation*. So when the Rust compiler surveyed the | ||
// dependence graph, it determined that nothing `[A]` imports changed since the | ||
// last build (and `[A]` itself has not changed either), so it chooses to reuse | ||
// the object code generated during the previous compilation. | ||
// | ||
// But that previous object code has an unresolved reference to `D`, and that | ||
// causes a link time failure! | ||
|
||
fn main() { | ||
foo::foo(); | ||
bar::baz(); | ||
} | ||
|
||
mod foo { | ||
|
||
// In cfail1, foo() gets inlined into main. | ||
// In cfail2, ThinLTO decides that foo() does not get inlined into main, and | ||
// instead bar() gets inlined into foo(). But faulty logic in our incr. | ||
// ThinLTO implementation thought that `main()` is unchanged and thus reused | ||
// the object file still containing a call to the now non-existant bar(). | ||
pub fn foo(){ | ||
bar() | ||
} | ||
|
||
// This function needs to be big so that it does not get inlined by ThinLTO | ||
// but *does* get inlined into foo() once it is declared `internal` in | ||
// cfail2. | ||
pub fn bar(){ | ||
println!("quux1"); | ||
println!("quux2"); | ||
println!("quux3"); | ||
println!("quux4"); | ||
println!("quux5"); | ||
println!("quux6"); | ||
println!("quux7"); | ||
println!("quux8"); | ||
println!("quux9"); | ||
} | ||
} | ||
|
||
mod bar { | ||
|
||
#[inline(never)] | ||
pub fn baz() { | ||
#[cfg(cfail1)] | ||
{ | ||
crate::foo::bar(); | ||
} | ||
} | ||
} |