Skip to content

Commit c93e847

Browse files
committed
Auto merge of #6336 - Eh2406:im-rs, r=alexcrichton
Persistent data structures by im-rs There has been a long standing "TODO: We should switch to persistent hash maps if we can" in the resolver. This PR introduces a dependency on [im-rs](bodil/im-rs#26) to provide persistent hash maps. It then uses `im_rc::OrdSet` to store the priority list of dependencies, instead of `std::BinaryHeap`, as cloning the `Heap` was one of the most expensive things we did. In Big O terms these changes are very big wins, in practice the improvement is small. This is do to a number of factors like, `std::collections` are very fast, N is usually only in the hundreds, we only clone when the borrow checker insists on it, and we use `RC` and other tricks to avoid cloning. I would advocate that we accept this PR, as: - Things are only going to get more complicated in the future. It would be nice to have Big O on our side. - When developing a new feature finding each place to add `RC` should be an afterthought not absolutely required before merge. (cc #6129 witch is stuck on this kind of per tick performance problem as well as other things). - As the code gets more complex, making sure we never break any of the tricks becomes harder. It would be easier to work on this if such mistakes are marginal instead of show stoppers. (cc #5130 a bug report of one of these bing removed and affecting `./x.py build` enough to trigger an investigation). - The resolver is already very complicated with a lot of moving parts to keep in your head at the same time, this will only get worse as we add features. There are a long list of tricks that are *critical* before and `~5%` after, it would be nice to consider if each one is worth the code complexity. (I can list some of the ones I have tried to remove but are still small wins, if that would help the discussion). Overall, I think it is worth doing now, but can see that if is not a slam dunk.
2 parents e192fdf + 1699a5b commit c93e847

File tree

4 files changed

+39
-25
lines changed

4 files changed

+39
-25
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ url = "1.1"
5959
clap = "2.31.2"
6060
unicode-width = "0.1.5"
6161
openssl = { version = '0.10.11', optional = true }
62+
im-rc = "12.1.0"
6263

6364
# A noop dependency that changes in the Rust repository, it's a bit of a hack.
6465
# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`

src/cargo/core/resolver/context.rs

+7-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use core::interning::InternedString;
55
use core::{Dependency, FeatureValue, PackageId, SourceId, Summary};
66
use util::CargoResult;
77
use util::Graph;
8+
use im_rc;
89

910
use super::errors::ActivateResult;
1011
use super::types::{ConflictReason, DepInfo, GraphNode, Method, RcList, RegistryQueryer};
@@ -19,12 +20,9 @@ pub use super::resolve::Resolve;
1920
// possible.
2021
#[derive(Clone)]
2122
pub struct Context {
22-
// TODO: Both this and the two maps below are super expensive to clone. We should
23-
// switch to persistent hash maps if we can at some point or otherwise
24-
// make these much cheaper to clone in general.
2523
pub activations: Activations,
26-
pub resolve_features: HashMap<PackageId, Rc<HashSet<InternedString>>>,
27-
pub links: HashMap<InternedString, PackageId>,
24+
pub resolve_features: im_rc::HashMap<PackageId, Rc<HashSet<InternedString>>>,
25+
pub links: im_rc::HashMap<InternedString, PackageId>,
2826

2927
// These are two cheaply-cloneable lists (O(1) clone) which are effectively
3028
// hash maps but are built up as "construction lists". We'll iterate these
@@ -36,16 +34,16 @@ pub struct Context {
3634
pub warnings: RcList<String>,
3735
}
3836

39-
pub type Activations = HashMap<(InternedString, SourceId), Rc<Vec<Summary>>>;
37+
pub type Activations = im_rc::HashMap<(InternedString, SourceId), Rc<Vec<Summary>>>;
4038

4139
impl Context {
4240
pub fn new() -> Context {
4341
Context {
4442
resolve_graph: RcList::new(),
45-
resolve_features: HashMap::new(),
46-
links: HashMap::new(),
43+
resolve_features: im_rc::HashMap::new(),
44+
links: im_rc::HashMap::new(),
4745
resolve_replacements: RcList::new(),
48-
activations: HashMap::new(),
46+
activations: im_rc::HashMap::new(),
4947
warnings: RcList::new(),
5048
}
5149
}

src/cargo/core/resolver/types.rs

+30-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::cmp::Ordering;
2-
use std::collections::{BinaryHeap, HashMap, HashSet};
2+
use std::collections::{HashMap, HashSet};
33
use std::ops::Range;
44
use std::rc::Rc;
55
use std::time::{Duration, Instant};
@@ -9,6 +9,8 @@ use core::{Dependency, PackageId, PackageIdSpec, Registry, Summary};
99
use util::errors::CargoResult;
1010
use util::Config;
1111

12+
use im_rc;
13+
1214
pub struct ResolverProgress {
1315
ticks: u16,
1416
start: Instant,
@@ -52,7 +54,11 @@ impl ResolverProgress {
5254
// with all the algorithm improvements.
5355
// If any of them are removed then it takes more than I am willing to measure.
5456
// So lets fail the test fast if we have ben running for two long.
55-
debug_assert!(self.ticks < 50_000);
57+
debug_assert!(
58+
self.ticks < 50_000,
59+
"got to 50_000 ticks in {:?}",
60+
self.start.elapsed()
61+
);
5662
// The largest test in our suite takes less then 30 sec
5763
// with all the improvements to how fast a tick can go.
5864
// If any of them are removed then it takes more than I am willing to measure.
@@ -299,48 +305,56 @@ impl Ord for DepsFrame {
299305
fn cmp(&self, other: &DepsFrame) -> Ordering {
300306
self.just_for_error_messages
301307
.cmp(&other.just_for_error_messages)
302-
.then_with(||
303-
// the frame with the sibling that has the least number of candidates
304-
// needs to get bubbled up to the top of the heap we use below, so
305-
// reverse comparison here.
306-
self.min_candidates().cmp(&other.min_candidates()).reverse())
308+
.reverse()
309+
.then_with(|| self.min_candidates().cmp(&other.min_candidates()))
307310
}
308311
}
309312

310-
/// Note that a `BinaryHeap` is used for the remaining dependencies that need
311-
/// activation. This heap is sorted such that the "largest value" is the most
312-
/// constrained dependency, or the one with the least candidates.
313+
/// Note that a `OrdSet` is used for the remaining dependencies that need
314+
/// activation. This set is sorted by how many candidates each dependency has.
313315
///
314316
/// This helps us get through super constrained portions of the dependency
315317
/// graph quickly and hopefully lock down what later larger dependencies can
316318
/// use (those with more candidates).
317319
#[derive(Clone)]
318-
pub struct RemainingDeps(BinaryHeap<DepsFrame>);
320+
pub struct RemainingDeps {
321+
/// a monotonic counter, increased for each new insertion.
322+
time: u32,
323+
/// the data is augmented by the insertion time.
324+
/// This insures that no two items will cmp eq.
325+
/// Forcing the OrdSet into a multi set.
326+
data: im_rc::OrdSet<(DepsFrame, u32)>,
327+
}
319328

320329
impl RemainingDeps {
321330
pub fn new() -> RemainingDeps {
322-
RemainingDeps(BinaryHeap::new())
331+
RemainingDeps {
332+
time: 0,
333+
data: im_rc::OrdSet::new(),
334+
}
323335
}
324336
pub fn push(&mut self, x: DepsFrame) {
325-
self.0.push(x)
337+
let insertion_time = self.time;
338+
self.data.insert((x, insertion_time));
339+
self.time += 1;
326340
}
327341
pub fn pop_most_constrained(&mut self) -> Option<(bool, (Summary, (usize, DepInfo)))> {
328-
while let Some(mut deps_frame) = self.0.pop() {
342+
while let Some((mut deps_frame, insertion_time)) = self.data.remove_min() {
329343
let just_here_for_the_error_messages = deps_frame.just_for_error_messages;
330344

331345
// Figure out what our next dependency to activate is, and if nothing is
332346
// listed then we're entirely done with this frame (yay!) and we can
333347
// move on to the next frame.
334348
if let Some(sibling) = deps_frame.remaining_siblings.next() {
335349
let parent = Summary::clone(&deps_frame.parent);
336-
self.0.push(deps_frame);
350+
self.data.insert((deps_frame, insertion_time));
337351
return Some((just_here_for_the_error_messages, (parent, sibling)));
338352
}
339353
}
340354
None
341355
}
342356
pub fn iter(&mut self) -> impl Iterator<Item = (&PackageId, Dependency)> {
343-
self.0.iter().flat_map(|other| other.flatten())
357+
self.data.iter().flat_map(|(other, _)| other.flatten())
344358
}
345359
}
346360

src/cargo/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ extern crate termcolor;
6262
extern crate toml;
6363
extern crate unicode_width;
6464
extern crate url;
65+
extern crate im_rc;
6566

6667
use std::fmt;
6768

0 commit comments

Comments
 (0)