Change:
From nightly-2020-02-28 (nightly-2020-02-27 was fine), I get link errors if I compile a cdylib with a thread local that isn't used, then compile again after referencing the thread local. To reproduce, create a.rs: Then run: Uncomment the commented line and rerun the above command. This produces the following link error: It's possible however that the underlying bug is actually older though, since at opt-level=1, I get link errors from 1.32.0 onwards (1.31.0 was fine). In that case, the Drop implementation isn't needed. Linking fails with: The exact symbol that's reported varies. Versions: rustc 1.43.0-nightly ( 2020-03-06) cc (Debian 9.2.1-16) 9.2.1 GNU ld (GNU Binutils for Debian) 2.33.1 <!-- TRIAGEBOTSTART --<!-- TRIAGEBOTASSIGNSTART --<!-- TRIAGEBOTASSIGNDATASTARTTRIAGEBOTASSIGNDATAEND --<!-- TRIAGEBOTASSIGNEND --<!-- TRIAGEBOTEND --
claim
Here's what appears to be happening: generate a CGU containing some drop glue, along with some globals (e.g. the "Dropping Foo" constant). When LTO runs, LLVM consults the ThinLTO index, and . This allows it to . This allows said constant (e.g. ) to be deleted entirely, since it cannot be accessed outside of the CGU. uncomment . Now, we end up needing to import this drop glue (since it gets called by the drop glue for ). We re-generate the module performing the import, but not the module we are importing from. freshly codegenned module ends up inlining a function from the re-used module, which references an constant from the re-used module. However, the post-optimization IR/ASM for the re-used module no longer contains this constant, since it was unused at the time the re-used module was optimized. Note that exact names imported seem to be extremely sensitive to local changes, so you might get different constants/containing functions. The general problem seems to be that LLVM looks 'backward' in the dependency graph during ThinLTO - that is, it looks at whether any modules import from the current module, not just what modules the current modules imports. I think there are a few possible solutions: a module goes from exporting nothing to exporting something, rebuild any modules which now import it. This is the most targeted fix, but I'm worried that it could be quite fragile. As far as I can tell, LLVM assumes that post-LTO files are never reused. Theoretically, LLVM could perform a more precise check to see which functions are actually 'exported' (e.g. used elsewhere) from a given ThinLTO CGU. the check in to rebuild both the 'importer' and 'importee' when the import map changes. That is, if module 'A' starts/stops importing from module 'B', we rebuild both A and B. This is a broader change than 1), and should hopefully continue to work even if LLVM gets 'smarter'. However, it could cause us to unnecessarily re-run codegen for some modules, which could have a significant performance impact. LLVM to be more conservative about deciding that a module has no exports. It's not clear to me how easy such a change would be, or whether upstream would accept it. Someone who's more familiar with LLVM should weigh in here.
ping icebreakers-llvm
Hey LLVM ICE-breakers! This bug has been identified as a good "LLVM ICE-breaking candidate". In case it's useful, here are some [instructions] for tackling these sorts of bugs. Maybe take a look? Thanks! <3 [instructions]: https://rust- cc
We have the first non working nightly which is , although it would be nice to get the first regresed commit or PR using .
Tagging as P-high until we know how bad this issue is.
From , and without really knowing what's going on, seems likely to me. It mentions removing MIR Drop terminators that will never be reached.
Seems my guess was wrong. I ran cargo-bisect-rustc (neat command BTW). It reported the regression with commit
For reference, is part of PR ; i.e. the latter PR either injected this bug, or tickled a latent bug in LLVM. (Which of the latter two cases we are in depends in part on how we resolve the by .)
Of the options outlined by , I think number 2 is best: I have started prototyping it on my laptop, since I am familiar with the code I in PR
My prototype is now done and appears to fix this case. I have not attempted to benchmark yet; I figure we can land it, or at least run it through , and see where it goes.
marking as stable-to-beta regression. The underlying bug here is old, but the change that exposes the bug here (PR ) has not yet made it to stable.

Discussion:
// If previous imports have been deleted, or we get an IO error // reading the file storing them, then we'll just use `None` as the // prev_import_map, which will force the code to be recompiled. <del> let prev = if path.exists() { ThinLTOImports::load_from_file(&path).ok() } else { None }; let curr = ThinLTOImports::from_thin_lto_data(data); </del> <ins> let prev = if path.exists() { ThinLTOImportMaps::load_from_file(&path).ok() } else { None }; let curr = ThinLTOImportMaps::from_thin_lto_data(data); </ins> (Some(path), prev, curr) } else { // If we don't compile incrementally, we don't need to load the // import data from LLVM. assert!(green_modules.is_empty()); <del> let curr = ThinLTOImports::default(); </del> <ins> let curr = ThinLTOImportMaps::default(); </ins> (None, None, curr) }; info!("thin LTO import map loaded");

let module_name = module_name_to_str(module_name); // If (1.) the module hasn't changed, and (2.) none of the modules <del> // it imports from has changed, *and* (3.) the import-set itself has // not changed from the previous compile when it was last // ThinLTO'ed, then we can re-use the post-ThinLTO version of the // module. Otherwise, freshly perform LTO optimization. </del> <ins> // it imports from nor exports to have changed, *and* (3.) the // import and export sets themselves have not changed from the // previous compile when it was last ThinLTO'ed, then we can re-use // the post-ThinLTO version of the module. Otherwise, freshly // perform LTO optimization. // // (Note that globally, the export set is just the inverse of the // import set.) </ins> // // This strategy means we can always save the computed imports as // canon: when we reuse the post-ThinLTO version, condition (3.)

// version, the current import set *is* the correct one, since we // are doing the ThinLTO in this current compilation cycle.) // <del> // See rust-lang/rust#59535. </del> <ins> // For more discussion, see rust-lang/rust#59535 (where the import // issue was discovered) and rust-lang/rust#69798 (where the // analogous export issue was discovered). </ins> if let (Some(prev_import_map), true) = (prev_import_map.as_ref(), green_modules.contains_key(module_name)) { assert!(cgcx.incr_comp_session_dir.is_some()); <del> let prev_imports = prev_import_map.modules_imported_by(module_name); let curr_imports = curr_import_map.modules_imported_by(module_name); </del> <ins> let prev_imports = prev_import_map.imports_of(module_name); let curr_imports = curr_import_map.imports_of(module_name); let prev_exports = prev_import_map.exports_of(module_name); let curr_exports = curr_import_map.exports_of(module_name); </ins> let imports_all_green = curr_imports .iter() .all(|imported_module| green_modules.contains_key(imported_module)); <ins> let exports_all_green = curr_exports .iter() .all(|exported_module| green_modules.contains_key(exported_module)); </ins> <del> if imports_all_green && equivalent_as_sets(prev_imports, curr_imports) { </del> <ins> if imports_all_green && equivalent_as_sets(prev_imports, curr_imports) && exports_all_green && equivalent_as_sets(prev_exports, curr_exports) { </ins> let work_product = green_modules[module_name].clone(); copy_jobs.push(work_product); info!(" - {}: re-used", module_name);

Ok(module) } <ins> /// Summarizes module import/export relationships used by LLVM's ThinLTO pass. /// /// Note that we tend to have two such instances of `ThinLTOImportMaps` in use: /// one loaded from a file that represents the relationships used during the /// compilation associated with the incremetnal build artifacts we are /// attempting to reuse, and another constructed via `from_thin_lto_data`, which /// captures the relationships of ThinLTO in the current compilation. </ins> #[derive(Debug, Default)] <del> pub struct ThinLTOImports { </del> <ins> pub struct ThinLTOImportMaps { </ins> // key = llvm name of importing module, value = list of modules it imports from imports: FxHashMap<String, Vec<String>>, <ins> // key = llvm name of exporting module, value = list of modules it exports to exports: FxHashMap<String, Vec<String>>, </ins> } <del> impl ThinLTOImports { fn modules_imported_by(&self, llvm_module_name: &str) -> &[String] { </del> <ins> impl ThinLTOImportMaps { /// Returns modules imported by `llvm_module_name` during some ThinLTO pass. fn imports_of(&self, llvm_module_name: &str) -> &[String] { </ins> self.imports.get(llvm_module_name).map(|v| &v[..]).unwrap_or(&[]) } <ins> /// Returns modules exported by `llvm_module_name` during some ThinLTO pass. fn exports_of(&self, llvm_module_name: &str) -> &[String] { self.exports.get(llvm_module_name).map(|v| &v[..]).unwrap_or(&[]) } </ins> fn save_to_file(&self, path: &Path) -> io::Result<()> { use std::io::Write; let file = File::create(path)?;

Ok(()) } <del> fn load_from_file(path: &Path) -> io::Result<ThinLTOImports> { </del> <ins> fn load_from_file(path: &Path) -> io::Result<ThinLTOImportMaps> { </ins> use std::io::BufRead; let mut imports = FxHashMap::default(); <del> let mut current_module = None; let mut current_imports = vec![]; </del> <ins> let mut exports: FxHashMap<_, Vec<_>> = FxHashMap::default(); let mut current_module: Option<String> = None; let mut current_imports: Vec<String> = vec![]; </ins> let file = File::open(path)?; for line in io::BufReader::new(file).lines() { let line = line?; if line.is_empty() { let importing_module = current_module.take().expect("Importing module not set"); <ins> for imported in &current_imports { exports.entry(imported.clone()).or_default().push(importing_module.clone()); } </ins> imports.insert(importing_module, mem::replace(&mut current_imports, vec![])); } else if line.starts_with(' ') { // Space marks an imported module

current_module = Some(line.trim().to_string()); } } <del> Ok(ThinLTOImports { imports }) </del> <ins> Ok(ThinLTOImportMaps { imports, exports }) </ins> } /// Loads the ThinLTO import map from ThinLTOData. <del> unsafe fn from_thin_lto_data(data: *const llvm::ThinLTOData) -> ThinLTOImports { </del> <ins> unsafe fn from_thin_lto_data(data: *const llvm::ThinLTOData) -> ThinLTOImportMaps { </ins> unsafe extern "C" fn imported_module_callback( payload: *mut libc::c_void, importing_module_name: *const libc::c_char, imported_module_name: *const libc::c_char, ) { <del> let map = &mut *(payload as *mut ThinLTOImports); </del> <ins> let map = &mut *(payload as *mut ThinLTOImportMaps); </ins> let importing_module_name = CStr::from_ptr(importing_module_name); let importing_module_name = module_name_to_str(&importing_module_name); let imported_module_name = CStr::from_ptr(imported_module_name);

.get_mut(importing_module_name) .unwrap() .push(imported_module_name.to_owned()); <ins> if !map.exports.contains_key(imported_module_name) { map.exports.insert(imported_module_name.to_owned(), vec![]); } map.exports .get_mut(imported_module_name) .unwrap() .push(importing_module_name.to_owned()); </ins> } <del> let mut map = ThinLTOImports::default(); </del> <ins> let mut map = ThinLTOImportMaps::default(); </ins> llvm::LLVMRustGetThinLTOModuleImports( data, imported_module_callback,

<ins> // revisions: cfail1 cfail2 // build-pass // rust-lang/rust#69798: // // This is analgous to cgu_invalidated_when_import_added, but it covers a // problem uncovered where a change to the *export* set caused a link failure // when reusing post-LTO optimized object code. pub struct Foo {} impl Drop for Foo { fn drop(&mut self) { println!("Dropping Foo"); } } #[no_mangle] pub extern "C" fn run() { thread_local! { pub static FOO : Foo = Foo { } ; } #[cfg(cfail2)] { FOO.with(|_f| ()) } } pub fn main() { run() } </ins>

<ins> // revisions: cfail1 cfail2 // build-pass // rust-lang/rust#69798: // // This is analgous to cgu_invalidated_when_export_added, but it covers the // other direction. This is analogous to cgu_invalidated_when_import_added: we // include it, because it may uncover bugs in variant implementation strategies. pub struct Foo {} impl Drop for Foo { fn drop(&mut self) { println!("Dropping Foo"); } } #[no_mangle] pub extern "C" fn run() { thread_local! { pub static FOO : Foo = Foo { } ; } #[cfg(cfail1)] { FOO.with(|_f| ()) } } pub fn main() { run() } </ins>