-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fast clone graph #7168
Fast clone graph #7168
Conversation
r? @ehuss (rust_highfive has picked a reviewer for you, use r? to override) |
☔ The latest upstream changes (presumably #7174) made this pull request unmergeable. Please resolve the merge conflicts. |
Rebased. |
/// This is a directed Graph structure. Each edge can have an `E` associated with it, | ||
/// but may have more then one or none. Furthermore, it is designed to be "append only" so that | ||
/// it can be queried as it would have bean when it was smaller. This allows a `reset_to` method | ||
/// that efficiently undoes the most reason modifications. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the comments here and the fields below be expanded to indicate what all the integer pointers are intended for? From reading this I don't have a great grasp of how this is implemented, and it looks pretty complicated internally. To clarify I don't doubt it, was just hoping to not have to read so closely below after reading this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add a new type for each use of usize
and some comments.
A fair bit of the complexity comes from the or none
support. This is needed as a lockfile knows all the packages eth package depends on but does not know the real Dependency
objects. This data structure supports interweaving of adding links with edge data and links that do not have data. We use both but we don't in practice interweave them. I wonder if there is a way to simplify the structure if we ban interweaving...
I'll try to get to this tomorrow morning, it looks like it's going to be pretty deep into data structure land so I'm not quite ready to do that today! |
dfad1cc
to
1b2f42d
Compare
Rebased, simplified, and added some comments. What else can be made clearer? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This version is indeed easier to read, thanks!
I'm still having difficulty getting through this, although resolve stuff is in general really hard to grok. I find myself lacking a lot of context though in the sense that I'd still love to see like a multi-paragraph-long description explaining the problem that this data structure is trying to solve.
I guess another way to put it is that the resolution phase of cargo has a lot of interesting patterns, and those constraints are informing what this data structure is going to look like. It'd be great to have that explicitly spelled out in comments as well as having specific comments as to the implementation and how it works. For exaple the reset_to
function seems like the whole crux of this dat structure but it has few comments to explain why it's doing what it's doing and why it works the way it does.
src/cargo/util/graph.rs
Outdated
|
||
/// connect `node`to `child` associating it with `edge`. | ||
/// Note that if this and `link` are used on the same graph | ||
/// odd things may happen when `reset_to` is called. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a bit of an odd restriction which may be a holdover from some old version of the resolver, would it be possible to only have either link
or add_edge
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
link
is only needed as a lockfile knows all the packages each package depends on but does not know the real Dependency
objects (hear). This could be a Graph<_, ()>
but it needs to go into a Resolve
so it needs to be Graph<_, Dependency>
. I am open to suggestions for how to work around this!
Having had a chance to sleep on it, I don't remember why I thought it would be a problem. I will just remove the "Note".
src/cargo/util/graph.rs
Outdated
/// - no overhead over using the `Graph` directly when modifying | ||
/// Is this too good to be true? There are two caveats: | ||
/// - It can only be modified using a strict "Stack Discipline", only modifying the biggest clone | ||
/// of the graph. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like an interesting restriction, but one you could subvert as well, right? Is this what the asserts are intended for in the borrow
function to dynamically assert this invariant?
src/cargo/util/graph.rs
Outdated
type Item = &'a E; | ||
|
||
fn next(&mut self) -> Option<&'a E> { | ||
while let Some(edge_link) = self.index.and_then(|old_index| self.graph.get(old_index)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the get
here required to be a fallible lookup? I'd imagine that if we're iterating all internal edge pointers should already be valid
.nodes | ||
.get(from) | ||
.and_then(|x| x.get(to).copied()) | ||
.filter(|idx| idx < &self.age.len_edges), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extra filtering done here in the view is somewhat odd, can you be sure to add comments (not sure where, just somewhere would be fine) as to why it's necessary?
Thank you for the on-point feedback. I added some comments and responded to some of your points. I did not yet have time for the multi-paragraph-long description. I will need to think through how to describe only the complexity required to read this module vs providing enough context to grok why it is what we need. You other points should become clearer if (when?) I describe the hole picture well. |
src/cargo/util/graph.rs
Outdated
} | ||
|
||
/// A RAII gard that allows getting `&` references to the prefix of a `Graph` as stored in a `StackGraph`. | ||
/// Other views of the inner `Graph` may have added things after this `StackGraph` was created. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm I'm not sure how this sentence is true, once this is created it freezes Graph
inside it because of the borrow on the RefCell
, right?
fn next(&mut self) -> Option<&'a E> { | ||
while let Some(edge_link) = self.index.and_then(|idx| { | ||
// Check that the `idx` points to something in `self.graph`. It may not if we are | ||
// looking at a smaller prefix of a larger graph. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is something I don't quite understand, I thought the point of reset_to
was that these sort of extraneous edges were pruned out? How do they linger around?
I wrote several paragraphs of words, I can no longer tell if they mean anything. Let me know if any of it makes sense and is worth having around. I added a test that I hope helps show the queries this supports. Having spent so long trying to describe what I meant by "Stack Discipline", I am now wondering if it would be possible to detect when a deep clone is needed automatically. At runtime given how the Resolver uses the |
☔ The latest upstream changes (presumably #7186) made this pull request unmergeable. Please resolve the merge conflicts. |
I am now setup on my new laptop, so I can do more accurate bench-marking. The version that does a deep clone any time it may be needed is slower then master when backtracking is involved. More experiments are needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the new comments! They look pretty good to me, and when you're ready I think this is basically good to go
src/cargo/util/graph.rs
Outdated
//! `dependency_graph_so_far.clone();`. To make this more annoying the first thing we try will | ||
//! probably work, and any work we do to prepare for the next iteration is wasted. If we had a | ||
//! `undo_activate` we could be much more efficient, completely remove the `.clone()` and just | ||
//! `undo_activate` if things tern out to not work. Unfortunately, making shore `undo_activate` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/shore/sure/
src/cargo/util/graph.rs
Outdated
|
||
/// This is a directed Graph structure. Each edge can have an `E` associated with it, | ||
/// but may have more then one or none. Furthermore, it is designed to be "append only" so that | ||
/// it can be queried as it would have bean when it was smaller. This allows a `reset_to` method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/bean/been/
Added code to determine when a deep clone is need. This cuts into the wins from this PR, but means that it does not need "Stack Discipline" to be correct, and is faster or the same on all the things I have tried. Over all, this has been a lot of work to beat |
Hm I may be getting lost again. I'm sort of confused, if this is all a pretty small win should we stick with the im-rs data structures? Those are presumably much easier to read/write, but especially with the new |
3 weeks ago when I started committing to this branch I was definitely planning to say: "this adds a large amount of complexity, is it worth it for the performance boost?" I seem to have forgotten to make that clere. Sorry! It is entirely reasonable for us to close this becust the wins are not worth the code. But let's get quantitative: % improve by commit and commendEtch average of 3 runs on my new laptop as idle as I can get it, and with
on cargo's repo
Is ~5% on no-op checks of Cargo big enough to justify all this new code? |
518028b
to
1f405c1
Compare
75b08b9
to
a16a022
Compare
☔ The latest upstream changes (presumably #7452) made this pull request unmergeable. Please resolve the merge conflicts. |
d38502f
to
fa8c4ee
Compare
☔ The latest upstream changes (presumably #7361) made this pull request unmergeable. Please resolve the merge conflicts. |
De-duplicate edges This is a quick fix for #7985. It is possible to have more than one dependency that connects two packages, if one is a dev and one a regular. The code has use a `Vec` to represent that potential multiplicity. This switches it to a `HashSet` to fix #7985. But if there is only a handful of ways we can have more than one then perhaps we can do something with less indirection/allocations. Note that #7168 (which was already abandoned) will need to be redesigned for whatever we do for this.
This is not going to get the attention that is needed to merge. And at this point I'd rather see the effort go into PubGrub not into optimizing this resolver. A lot of ink has gone into it, a more responsible maintainer would turn it into documentation before closing. But I am not that responsible. |
This is a redesign of the
Graph
used by theResolver
. TheResolver
is usually invoked with a lockfile, so the first thing we try will work, and any work we to to prepare for backtracking is wasted. A planeGraph
will have anO(n)
clone, way too expensive. AnRc<Graph>
hasO(1)
clone but is no savings as we just end up withO(n)
make_mut. Before the PR we have aGraph
built from im-rs parts, this gives useO(1)
clone at onlyO(ln(n))
overhead on every operation. This PRsStackGraph
hasO(ln(number of clones))
clone and no overhead! There is a catch, if we have to backtrack then we do aO(delta)
reset. In effect this PR moves work from the happy "lockfile" path to the "backtracking" path.