Skip to content

Commit ccd2383

Browse files
committed
Auto merge of #67020 - pnkfelix:issue-59535-accumulate-past-lto-imports, 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.
2 parents 01a4650 + 42b00a4 commit ccd2383

File tree

3 files changed

+250
-10
lines changed

3 files changed

+250
-10
lines changed

src/librustc_codegen_llvm/back/lto.rs

+114-10
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@ use rustc::hir::def_id::LOCAL_CRATE;
1616
use rustc::middle::exported_symbols::SymbolExportLevel;
1717
use rustc::session::config::{self, Lto};
1818
use rustc::util::common::time_ext;
19-
use rustc_data_structures::fx::FxHashMap;
19+
use rustc_data_structures::fx::{FxHashSet, FxHashMap};
2020
use rustc_codegen_ssa::{RLIB_BYTECODE_EXTENSION, ModuleCodegen, ModuleKind};
2121
use log::{info, debug};
2222

2323
use std::ffi::{CStr, CString};
24+
use std::fs::File;
25+
use std::io;
26+
use std::mem;
27+
use std::path::Path;
2428
use std::ptr;
2529
use std::slice;
2630
use std::sync::Arc;
2731

32+
/// We keep track of past LTO imports that were used to produce the current set
33+
/// of compiled object files that we might choose to reuse during this
34+
/// compilation session.
35+
pub const THIN_LTO_IMPORTS_INCR_COMP_FILE_NAME: &str = "thin-lto-past-imports.bin";
36+
2837
pub fn crate_type_allows_lto(crate_type: config::CrateType) -> bool {
2938
match crate_type {
3039
config::CrateType::Executable |
@@ -472,13 +481,26 @@ fn thin_lto(cgcx: &CodegenContext<LlvmCodegenBackend>,
472481

473482
info!("thin LTO data created");
474483

475-
let import_map = if cgcx.incr_comp_session_dir.is_some() {
476-
ThinLTOImports::from_thin_lto_data(data)
484+
let (import_map_path, prev_import_map, curr_import_map) =
485+
if let Some(ref incr_comp_session_dir) = cgcx.incr_comp_session_dir
486+
{
487+
let path = incr_comp_session_dir.join(THIN_LTO_IMPORTS_INCR_COMP_FILE_NAME);
488+
// If previous imports have been deleted, or we get an IO error
489+
// reading the file storing them, then we'll just use `None` as the
490+
// prev_import_map, which will force the code to be recompiled.
491+
let prev = if path.exists() {
492+
ThinLTOImports::load_from_file(&path).ok()
493+
} else {
494+
None
495+
};
496+
let curr = ThinLTOImports::from_thin_lto_data(data);
497+
(Some(path), prev, curr)
477498
} else {
478499
// If we don't compile incrementally, we don't need to load the
479500
// import data from LLVM.
480501
assert!(green_modules.is_empty());
481-
ThinLTOImports::default()
502+
let curr = ThinLTOImports::default();
503+
(None, None, curr)
482504
};
483505
info!("thin LTO import map loaded");
484506

@@ -502,18 +524,36 @@ fn thin_lto(cgcx: &CodegenContext<LlvmCodegenBackend>,
502524
for (module_index, module_name) in shared.module_names.iter().enumerate() {
503525
let module_name = module_name_to_str(module_name);
504526

505-
// If the module hasn't changed and none of the modules it imports
506-
// from has changed, we can re-use the post-ThinLTO version of the
507-
// module.
508-
if green_modules.contains_key(module_name) {
509-
let imports_all_green = import_map.modules_imported_by(module_name)
527+
// If (1.) the module hasn't changed, and (2.) none of the modules
528+
// it imports from has changed, *and* (3.) the import-set itself has
529+
// not changed from the previous compile when it was last
530+
// ThinLTO'ed, then we can re-use the post-ThinLTO version of the
531+
// module. Otherwise, freshly perform LTO optimization.
532+
//
533+
// This strategy means we can always save the computed imports as
534+
// canon: when we reuse the post-ThinLTO version, condition (3.)
535+
// ensures that the curent import set is the same as the previous
536+
// one. (And of course, when we don't reuse the post-ThinLTO
537+
// version, the current import set *is* the correct one, since we
538+
// are doing the ThinLTO in this current compilation cycle.)
539+
//
540+
// See rust-lang/rust#59535.
541+
if let (Some(prev_import_map), true) =
542+
(prev_import_map.as_ref(), green_modules.contains_key(module_name))
543+
{
544+
assert!(cgcx.incr_comp_session_dir.is_some());
545+
546+
let prev_imports = prev_import_map.modules_imported_by(module_name);
547+
let curr_imports = curr_import_map.modules_imported_by(module_name);
548+
let imports_all_green = curr_imports
510549
.iter()
511550
.all(|imported_module| green_modules.contains_key(imported_module));
512551

513-
if imports_all_green {
552+
if imports_all_green && equivalent_as_sets(prev_imports, curr_imports) {
514553
let work_product = green_modules[module_name].clone();
515554
copy_jobs.push(work_product);
516555
info!(" - {}: re-used", module_name);
556+
assert!(cgcx.incr_comp_session_dir.is_some());
517557
cgcx.cgu_reuse_tracker.set_actual_reuse(module_name,
518558
CguReuse::PostLto);
519559
continue
@@ -527,10 +567,33 @@ fn thin_lto(cgcx: &CodegenContext<LlvmCodegenBackend>,
527567
}));
528568
}
529569

570+
// Save the curent ThinLTO import information for the next compilation
571+
// session, overwriting the previous serialized imports (if any).
572+
if let Some(path) = import_map_path {
573+
if let Err(err) = curr_import_map.save_to_file(&path) {
574+
let msg = format!("Error while writing ThinLTO import data: {}", err);
575+
return Err(write::llvm_err(&diag_handler, &msg));
576+
}
577+
}
578+
530579
Ok((opt_jobs, copy_jobs))
531580
}
532581
}
533582

583+
/// Given two slices, each with no repeat elements. returns true if and only if
584+
/// the two slices have the same contents when considered as sets (i.e. when
585+
/// element order is disregarded).
586+
fn equivalent_as_sets(a: &[String], b: &[String]) -> bool {
587+
// cheap path: unequal lengths means cannot possibly be set equivalent.
588+
if a.len() != b.len() { return false; }
589+
// fast path: before building new things, check if inputs are equivalent as is.
590+
if a == b { return true; }
591+
// slow path: general set comparison.
592+
let a: FxHashSet<&str> = a.iter().map(|s| s.as_str()).collect();
593+
let b: FxHashSet<&str> = b.iter().map(|s| s.as_str()).collect();
594+
a == b
595+
}
596+
534597
pub(crate) fn run_pass_manager(cgcx: &CodegenContext<LlvmCodegenBackend>,
535598
module: &ModuleCodegen<ModuleLlvm>,
536599
config: &ModuleConfig,
@@ -832,6 +895,47 @@ impl ThinLTOImports {
832895
self.imports.get(llvm_module_name).map(|v| &v[..]).unwrap_or(&[])
833896
}
834897

898+
fn save_to_file(&self, path: &Path) -> io::Result<()> {
899+
use std::io::Write;
900+
let file = File::create(path)?;
901+
let mut writer = io::BufWriter::new(file);
902+
for (importing_module_name, imported_modules) in &self.imports {
903+
writeln!(writer, "{}", importing_module_name)?;
904+
for imported_module in imported_modules {
905+
writeln!(writer, " {}", imported_module)?;
906+
}
907+
writeln!(writer)?;
908+
}
909+
Ok(())
910+
}
911+
912+
fn load_from_file(path: &Path) -> io::Result<ThinLTOImports> {
913+
use std::io::BufRead;
914+
let mut imports = FxHashMap::default();
915+
let mut current_module = None;
916+
let mut current_imports = vec![];
917+
let file = File::open(path)?;
918+
for line in io::BufReader::new(file).lines() {
919+
let line = line?;
920+
if line.is_empty() {
921+
let importing_module = current_module
922+
.take()
923+
.expect("Importing module not set");
924+
imports.insert(importing_module,
925+
mem::replace(&mut current_imports, vec![]));
926+
} else if line.starts_with(" ") {
927+
// Space marks an imported module
928+
assert_ne!(current_module, None);
929+
current_imports.push(line.trim().to_string());
930+
} else {
931+
// Otherwise, beginning of a new module (must be start or follow empty line)
932+
assert_eq!(current_module, None);
933+
current_module = Some(line.trim().to_string());
934+
}
935+
}
936+
Ok(ThinLTOImports { imports })
937+
}
938+
835939
/// Loads the ThinLTO import map from ThinLTOData.
836940
unsafe fn from_thin_lto_data(data: *const llvm::ThinLTOData) -> ThinLTOImports {
837941
unsafe extern "C" fn imported_module_callback(payload: *mut libc::c_void,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// revisions: cfail1 cfail2
2+
// compile-flags: -O -Zhuman-readable-cgu-names -Cllvm-args=-import-instr-limit=10
3+
// build-pass
4+
5+
// rust-lang/rust#59535:
6+
//
7+
// This is analgous to cgu_invalidated_when_import_removed.rs, but it covers
8+
// the other direction:
9+
//
10+
// We start with a call-graph like `[A] -> [B -> D] [C]` (where the letters are
11+
// functions and the modules are enclosed in `[]`), and add a new call `D <- C`,
12+
// yielding the new call-graph: `[A] -> [B -> D] <- [C]`
13+
//
14+
// The effect of this is that the compiler previously classfied `D` as internal
15+
// and the import-set of `[A]` to be just `B`. But after adding the `D <- C` call,
16+
// `D` is no longer classified as internal, and the import-set of `[A]` becomes
17+
// both `B` and `D`.
18+
//
19+
// We check this case because an early proposed pull request included an
20+
// assertion that the import-sets monotonically decreased over time, a claim
21+
// which this test case proves to be false.
22+
23+
fn main() {
24+
foo::foo();
25+
bar::baz();
26+
}
27+
28+
mod foo {
29+
30+
// In cfail1, ThinLTO decides that foo() does not get inlined into main, and
31+
// instead bar() gets inlined into foo().
32+
// In cfail2, foo() gets inlined into main.
33+
pub fn foo(){
34+
bar()
35+
}
36+
37+
// This function needs to be big so that it does not get inlined by ThinLTO
38+
// but *does* get inlined into foo() when it is declared `internal` in
39+
// cfail1 (alone).
40+
pub fn bar(){
41+
println!("quux1");
42+
println!("quux2");
43+
println!("quux3");
44+
println!("quux4");
45+
println!("quux5");
46+
println!("quux6");
47+
println!("quux7");
48+
println!("quux8");
49+
println!("quux9");
50+
}
51+
}
52+
53+
mod bar {
54+
55+
#[inline(never)]
56+
pub fn baz() {
57+
#[cfg(cfail2)]
58+
{
59+
crate::foo::bar();
60+
}
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// revisions: cfail1 cfail2
2+
// compile-flags: -O -Zhuman-readable-cgu-names -Cllvm-args=-import-instr-limit=10
3+
// build-pass
4+
5+
// rust-lang/rust#59535:
6+
//
7+
// Consider a call-graph like `[A] -> [B -> D] <- [C]` (where the letters are
8+
// functions and the modules are enclosed in `[]`)
9+
//
10+
// In our specific instance, the earlier compilations were inlining the call
11+
// to`B` into `A`; thus `A` ended up with a external reference to the symbol `D`
12+
// in its object code, to be resolved at subsequent link time. The LTO import
13+
// information provided by LLVM for those runs reflected that information: it
14+
// explicitly says during those runs, `B` definition and `D` declaration were
15+
// imported into `[A]`.
16+
//
17+
// The change between incremental builds was that the call `D <- C` was removed.
18+
//
19+
// That change, coupled with other decisions within `rustc`, made the compiler
20+
// decide to make `D` an internal symbol (since it was no longer accessed from
21+
// other codegen units, this makes sense locally). And then the definition of
22+
// `D` was inlined into `B` and `D` itself was eliminated entirely.
23+
//
24+
// The current LTO import information reported that `B` alone is imported into
25+
// `[A]` for the *current compilation*. So when the Rust compiler surveyed the
26+
// dependence graph, it determined that nothing `[A]` imports changed since the
27+
// last build (and `[A]` itself has not changed either), so it chooses to reuse
28+
// the object code generated during the previous compilation.
29+
//
30+
// But that previous object code has an unresolved reference to `D`, and that
31+
// causes a link time failure!
32+
33+
fn main() {
34+
foo::foo();
35+
bar::baz();
36+
}
37+
38+
mod foo {
39+
40+
// In cfail1, foo() gets inlined into main.
41+
// In cfail2, ThinLTO decides that foo() does not get inlined into main, and
42+
// instead bar() gets inlined into foo(). But faulty logic in our incr.
43+
// ThinLTO implementation thought that `main()` is unchanged and thus reused
44+
// the object file still containing a call to the now non-existant bar().
45+
pub fn foo(){
46+
bar()
47+
}
48+
49+
// This function needs to be big so that it does not get inlined by ThinLTO
50+
// but *does* get inlined into foo() once it is declared `internal` in
51+
// cfail2.
52+
pub fn bar(){
53+
println!("quux1");
54+
println!("quux2");
55+
println!("quux3");
56+
println!("quux4");
57+
println!("quux5");
58+
println!("quux6");
59+
println!("quux7");
60+
println!("quux8");
61+
println!("quux9");
62+
}
63+
}
64+
65+
mod bar {
66+
67+
#[inline(never)]
68+
pub fn baz() {
69+
#[cfg(cfail1)]
70+
{
71+
crate::foo::bar();
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)