Resolver: Parallelize the import resolution loop - #158845
Resolver: Parallelize the import resolution loop#158845LorrensP-2158466 wants to merge 7 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. |
This comment has been minimized.
This comment has been minimized.
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
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (b4cc008): comparison URL. Overall result: ❌✅ regressions and improvements - 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 rustc-perf 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 2.2%, secondary 3.0%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (primary 3.6%, secondary -4.5%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis perf run didn't have relevant results for this metric. Bootstrap: 487.635s -> 488.963s (0.27%) |
|
Nice, the conditional |
|
Wait, why does |
I think I already explained why we need this, though I can't find. Basically, in the functions concerned with retrieving a resolution, we use fn resolutions(&self, module: Module<'ra>) -> &'ra Resolutions<'ra> {
if !module.is_local() {
// as long as 1 thread is building this external table, all other threads will wait
module.populate_on_access.call_once(|| {
*module.lazy_resolutions.borrow_mut_unchecked() =
self.build_reduced_graph_external(module.expect_extern());
});
}
&module.0.0.lazy_resolutions
}
fn resolution(
&self,
module: Module<'ra>,
key: BindingKey,
) -> Option<ReadGuard<'ra, NameResolution<'ra>>> {
self.resolutions(module).borrow().get(&key).map(|resolution| resolution.0.borrow())
}These get called during speculative resolution, so the borrow counter needs to be synchronized. We use type Resolutions<'ra> = CmRefCell<FxIndexMap<BindingKey, NameResolutionRef<'ra>>>;
pub(crate) type NameResolutionRef<'ra> = Interned<'ra, CmRefCell<NameResolution<'ra>>>; |
…henkov Resolve: more preperation work for parallelizing the import resolution loop This is basically rust-lang/rust#158845 but we do not: - actually use `par_slice` because: - we do not migrate `CmRefCell` to use `RwLocks` (yet) because of perf reasons. The resolution loop is now in place instead of recollecting indeterminate imports. r? @petrochenkov
|
I see, it is due to the remaining Could you also extract the structures from |
Yeah, but we still need interior mutability for those 2 structures during speculative resolution, not to mention the other places it is used for // Not all fields, only relevant.
struct ModuleData<'ra> {
/// Mapping between names and their (possibly in-progress) resolutions in this module.
/// Resolutions in modules from other crates are not populated until accessed.
lazy_resolutions: Resolutions<'ra>,
/// Macro invocations that can expand into items in this module.
unexpanded_invocations: CmRefCell<FxHashSet<LocalExpnId>>,
glob_importers: CmRefCell<Vec<Import<'ra>>>,
globs: CmRefCell<Vec<Import<'ra>>>,
/// Used to memoize the traits in this module for faster searches through all traits in scope.
traits: CmRefCell<
Option<Box<[(Symbol, Decl<'ra>, Option<Module<'ra>>, bool /* lint ambiguous */)]>>,
>,
}So we need some structure that only allows reads during speculative resolution, and somewhow switches to interior mutability when not in speculative resolution. I think for some of the fields, something like a |
If something needs mutability during speculative resolution, it doesn't use |
|
I am sorry, I worded it wrong:
I meant that we need read access during speculative resolution and interior mutability elsewhere for things wrapped in
So:
While this is true, that is thus only half of it. So we need to find some very smart datastructure that can be synchronized or not depending the use of it. Which is very unsafe |
Yes, but it doesn't mean the data in |
|
After a lot of brainstorming I came up with an API like this (not detailed though): /// A cell that supports:
/// - mutable access in normal resolution (single-threaded)
/// - shared access in speculative resolution (multi-threaded)
struct CmRefCell<T> {
// mimic RefCell but with some tricks.
}
enum CmBorrow<'a, T> {
/// Normal resolution: RefCell-like borrow tracking
Normal(Ref<'a, T>),
/// Speculative resolution: plain shared reference
Speculative(&'a T),
}
impl<T> CmRefCell<T> {
fn borrow(&self, resolver: &CmResolver<'_, '_, '_>) -> CmBorrow<'_, T> {
if resolver.is_speculative() {
CmBorrow::Speculative(/* shared access */)
} else {
CmBorrow::Normal(/* RefCell borrow */)
}
}
fn borrow_mut(&self, resolver: &CmResolver<'_, '_, '_>) -> RefMut<'_, T> {
assert!(!resolver.is_speculative());
/* mutable access */
}
}Which has no synchronization because we do not need to mutate anything during speculative resolution, but we can still read. During non-speculative resolution we have the semantics we always had and I have a feeling this could work, is this something that you were thinking of as well? Though unsafe is required here. |
|
I am currently merging it over to a seperate crate to check with miri, but in the case you want to take a look and maybe do a perf run, @rustbot ready. Note it is not done, i am still working on it. |
67b5d60 to
c63be60
Compare
…n. When in speculative resolution, only shared borrows are allowed. An actual conditional `RefCell`.
c63be60 to
c25cae6
Compare
Same as #158845 (comment), I cannot answer without repeating the work and trying to implement in myself, but the high-level design seems plausible. |
|
|
||
| /// Macro invocations that can expand into items in this module. | ||
| unexpanded_invocations: CmRefCell<FxHashSet<LocalExpnId>>, | ||
| unexpanded_invocations: CacheRefCell<FxHashSet<LocalExpnId>>, |
There was a problem hiding this comment.
This is temporary, I assume?
unexpanded_invocations should never be modified from speculative resolution.
There was a problem hiding this comment.
Yes, I missed this one.
| // `Ref` argument doesn't hold immutability for its whole scope, only until it drops. | ||
| // `NonNull` is also covariant over `T`, just like we would have with `&T`. | ||
| value: NonNull<T>, | ||
| borrow: Option<&'b Cell<isize>>, |
There was a problem hiding this comment.
Is it possible to avoid copypasting std::cell internals, and instead wrap the standard cells and their refs?
There was a problem hiding this comment.
Not really without paying branch costs for every deref.
This:
enum CmBorrow<'b>{
Speculative(&'b T),
Normal(Ref<'b, T>),
}Would have conditionals on every deref (even fully optimized), which seems undesirable to me if its "pretty easy" to not have to do that, especially for deref/as_ref things.
It would be nice if we could reuse std::cell logic, but that is not possible.
There was a problem hiding this comment.
Let's maybe try it with the branch first, and then benchmark whether avoiding this branch matters.
There was a problem hiding this comment.
Okey (thought that I could "look ahead" :D)
There was a problem hiding this comment.
Oops, I totally forgot (sorry) that this:
It would be nice if we could reuse std::cell logic, but that is not possible.
means we can't create a Ref(Mut), so because I had to replicate it, I thought that I might as well share the pointer
| /// | ||
| /// - If the resulting `RefOrMut` may ever be accessed from, or sent to, more than | ||
| /// one thread, it **must** be constructed with `mutable = false`, and must only | ||
| /// ever be used to obtain shared (`&T`) access for its entire lifetime. |
There was a problem hiding this comment.
Is the second part ("must only ever be used to obtain shared (&T) access") something that the user must maintain? Or it will happen automatically due to the mutable flag checks?
There was a problem hiding this comment.
If the user intends to send RefOrMut across threads, it must pass in mutable = false, to ensure that a &mut T can never be obtained.
|
I've finally read the safety comments in detail, they look more convincing now. |
I'll think that I will extend them a bit and add a comment to the |
…safety) comments for `ref_mut` structures
|
Updated the comments and used |
| // Soundness depends entirely on proper usage of the `assert_speculative` field in the | ||
| // `Resolver`. This means that whenever we hold a `CmRef::Normal` or `cell::RefMut` and | ||
| // flip the switch on `assert_speculative` we also drop these borrows. | ||
| /// A wrapper around `RefCell` semantics that only allows writes (mutable borrows) based on a condition in the resolver. | ||
| #[derive(Default)] | ||
| pub(crate) struct CmRefCell<T> { | ||
| borrow: Cell<isize>, | ||
| value: UnsafeCell<T>, | ||
| inner: RefCell<T>, | ||
| } |
There was a problem hiding this comment.
Though I am really inclined to just switch between isize and AtomicIsize. That way we can never go wrong with aliased borrows
View all comments
Follow up of #159440. This pr implements the parallel part of
par_for_each_slice. And implementsDynSendandDynSyncforRefOrMutandCmCellto make the call topar_for_each_slicecompile.This is the bare minimum to make the parallel loop work and is not at all optimized, this will follow :).
r? @petrochenkov