Resolver: Parallelize the import resolution loop#158845
Resolver: Parallelize the import resolution loop#158845LorrensP-2158466 wants to merge 2 commits into
Conversation
|
Lets see what CI says. I have tried to add comments to changes to show my thought process behind them, but i'll make another pass here as well to be sure. |
| }) | ||
| } | ||
|
|
||
| pub fn par_filter_map<I: DynSend, T: IntoIterator<Item = I>, R: DynSend, C: FromIterator<R>>( |
There was a problem hiding this comment.
almost a mirror of filter_map just with flatten for the nested Option.
There was a problem hiding this comment.
Can we keep the Nones in the map to avoid introducing this?
write_import_resolutions (or whatever place consumes the resulting table) can then skip them.
There was a problem hiding this comment.
But ideally we'd do everything in place with par_slice, of course.
| match self.get_extern_module_with_lock(parent_id, extern_map_lock) { | ||
| Some(module) => break module.expect_extern(), | ||
| None => parent_id = self.tcx.parent(parent_id), | ||
| } |
There was a problem hiding this comment.
this previously whent through get_module, but since we dont have reentrant RwLocks and this expects an external module, I decided to do an immediate recursive call with the lock (see line 130 on why)
| let determined_imports = Lock::new(Vec::new()); | ||
| let indeterminate_imports = Lock::new(Vec::new()); |
There was a problem hiding this comment.
We could also collect all imports from the parallel loop below with their determinacy and optional resolution and then split them in write_import_resolution.
| PendingDecl::Ready(Some(import_decl)) => { | ||
| PendingDecl::Ready(Some(decl)) => { | ||
| // We need the `target`, `source` can be extracted | ||
| let import_decl = this.new_import_decl(decl, import); |
There was a problem hiding this comment.
This was previously in resolve_import causing arena allocations in parellel loop on a non-concurrent arena.
There was a problem hiding this comment.
This change can also be merged before the main parallelization PR.
This comment has been minimized.
This comment has been minimized.
| use crate::Resolver; | ||
|
|
||
| /// A wrapper around a mutable reference that conditionally allows mutable access. | ||
| pub(crate) struct RefOrMut<'a, T> { |
There was a problem hiding this comment.
in principle, anything that returns a mutable reference should now be unsafe because of the DynSync/Send because the caller must guarentee that we are in single-threaded mode. (resolver does that but still).
| // FIXME: this should be eliminated in the process of migration | ||
| // to parallel name resolution. |
There was a problem hiding this comment.
we'll probably require it untill be have 2 seperate fields for resolutions in modules. But I couldn't create a nice way of doing that and then having the 3 resolutions methods work.
This comment has been minimized.
This comment has been minimized.
17bd1cb to
9aa1069
Compare
This comment has been minimized.
This comment has been minimized.
9aa1069 to
a45ba86
Compare
|
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…r=<try> Resolver: Parallelize the import resolution loop
| } | ||
|
|
||
| #[inline(always)] | ||
| #[track_caller] |
There was a problem hiding this comment.
These track callers can cause unintended regressions in other places in the compiler, it's better to benchmark this change separately.
| ) { | ||
| let arenas = Resolver::arenas(); | ||
| let arenas = Resolver::new_arenas(); | ||
| let extern_arenas = Resolver::new_arenas(); |
There was a problem hiding this comment.
I expected having new locked fields inside the existing ResolverArenas, because we don't need to lock the whole ResolverArenas to do something with one field.
But perhaps this is indeed more convenient for a start.
There was a problem hiding this comment.
Yeah, this was just to make it work.
| // Safety check to make sure we don't allocate during speculative resolution. | ||
| // The arenas are not concurrency safe. | ||
| #[track_caller] | ||
| fn arenas(&self) -> &'ra ResolverArenas<'ra> { |
There was a problem hiding this comment.
Hm, how does this improve safety if the arenas field is still available for use without any checks?
There was a problem hiding this comment.
I have no idea how I could enforce that. I just changed every direct use to use this method instead.
I first made a seperate struct CondAccess which required the resolver as an argument to access the underlying T. But the code started to look very weird:
self.arenas.get(self)...There was a problem hiding this comment.
I wonder why accesses to arenas compile if the arenas are not actually thread safe.
Note that the same arena types are actively used from other parts of the compiler where the parallelism is already enabled.
There was a problem hiding this comment.
Because I unsafely implemented DynSend and DynSync for CmResolver. otherwise this all won't work. THere are a lot of fields in the Resolver which are not thread safe, but we only allow mutable access to those fields if specified by the creator of CmResolver (so safe unsafe...). Arena can be mutated through &self.
also (i think):
unsafe impl<T: Send> Send for TypedArena<T> {}
There was a problem hiding this comment.
Note that the same arena types are actively used from other parts of the compiler where the parallelism is already enabled.
pub arena: &'tcx WorkerLocal<Arena<'tcx>>,
pub hir_arena: &'tcx WorkerLocal<hir::Arena<'tcx>>,Other parts of the compiler use per-thread arenas.
There was a problem hiding this comment.
That should do the trick indeed. Did not know of that.
We could probably use it for the cycle_detector as well, no?
|
|
||
| // FIXME: These are cells for caches that can be populated even during speculative resolution, | ||
| // and should be replaced with mutexes, atomics, or other synchronized data when migrating to | ||
| // parallel name resolution. |
There was a problem hiding this comment.
Let's keep all the old names (CacheCell, CacheRefCell and also CmRefCell and methods like borrow_mut_unchecked) to avoid mass renaming things in this PR.
| /// A wrapper around a mutable reference that conditionally allows mutable access. | ||
| pub(crate) struct RefOrMut<'a, T> { | ||
| p: &'a mut T, | ||
| p: *mut T, |
There was a problem hiding this comment.
What breaks if we don't make this change?
There was a problem hiding this comment.
Because of reborrow_ref there is no way to "copy" the &mut T through &self.
| pub(crate) struct CmRwLock<T>(RwLock<T>); | ||
|
|
||
| /// SAFETY: We wrap a `RwLock`. | ||
| unsafe impl<T> DynSync for CmRwLock<T> {} |
There was a problem hiding this comment.
Is it necessary to add this? Won't this impl be derived automatically?
| pub(crate) fn build_reduced_graph_external( | ||
| &self, | ||
| module: ExternModule<'ra>, | ||
| ) -> FxIndexMap<BindingKey, NameResolutionRef<'ra>> { |
There was a problem hiding this comment.
Changes like this can be merged before the main parallelization PR.
| let used = self.process_macro_use_imports(item, module); | ||
| let decl = self.r.arenas.new_pub_def_decl(module.res().unwrap(), sp, expansion); | ||
| let decl = | ||
| self.r.extern_arenas.lock().new_pub_def_decl(module.res().unwrap(), sp, expansion); |
There was a problem hiding this comment.
build_reduced_graph_for_extern_crate is never called from speculative import resolution, not sure why extern_arenas are used here.
There was a problem hiding this comment.
Because of the *_extern_crate. I wanted it to be uniform.
We should probably change the extern_arenas to speculative_arena (or smth else) then?
There was a problem hiding this comment.
Or sync_arena (if the separate arena is necessary at all), I'd rather use it only where the locking is actually necessary.
There was a problem hiding this comment.
it would be nice to have a datastructure that only does synchronised access during speculative resolution. That would allow us to use only 1 arena.
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (3df1fed): comparison URL. Overall result: ❌ regressions - please read:Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf. Next, please: If you can, justify the regressions found in this try perf run in writing along with @bors rollup=never Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary -0.4%, secondary -0.5%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (secondary 21.9%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (secondary 0.0%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 491.018s -> 490.555s (-0.09%) |
|
Yeah, a small instr count regression for import-heavy crates like libc. What is more interesting that it's a (wall time) regression for |
This comment has been minimized.
This comment has been minimized.
a45ba86 to
c2e0f3d
Compare
…l import resolution.
c2e0f3d to
94bb460
Compare
With the accompanying changes to make datastructures concurrent: - CmRefCell now wraps a `rustc_datastructures::sync::RwLock` because of the borrow counters (does introduce synchronized access in single-threaded mode) - Cache(Ref)Cells are now changed into atomics/(rw)locks - populating external resolution tables now use `Once` to ensure only 1 thread builds the table and all others wait if they need it This is the bare minimum to make the loop work and is not at all optimized, this will follow :).
94bb460 to
64da212
Compare
|
The job Click to see the possible cause of the failure (guessed by this bot) |
View all comments
Parallelize the import resolution loop using
par_filter_map(introduced in this pr).With the accompanying changes to make datastructures concurrent:
Onceto ensure only 1 thread builds the table and all others wait if they need itThis is the bare minimum to make the parallel loop work and is not at all optimized, this will follow :).
r? @petrochenkov