Skip to content
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

BTreeMap mutable iterators should not take any reference to visited nodes during iteration #73971

Merged
merged 2 commits into from
Sep 9, 2020

Conversation

ssomers
Copy link
Contributor

@ssomers ssomers commented Jul 2, 2020

Fixes #73915, overlapping mutable references during BTreeMap iteration

r? @RalfJung

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 2, 2020
@ssomers
Copy link
Contributor Author

ssomers commented Jul 3, 2020

benchcmp old newer --threshold 5

 name                                   old ns/iter  newer ns/iter  diff ns/iter   diff %  speedup
 btree::map::find_rand_100              17           18                        1    5.88%   x 0.94
 btree::map::find_seq_10_000            39           43                        4   10.26%   x 0.91
 btree::map::insert_rand_100            41           36                       -5  -12.20%   x 1.14
 btree::map::insert_rand_10_000         41           36                       -5  -12.20%   x 1.14
 btree::map::insert_seq_100             49           45                       -4   -8.16%   x 1.09
 btree::map::insert_seq_10_000          94           102                       8    8.51%   x 0.92
 btree::map::iter_0                     1,724        1,509                  -215  -12.47%   x 1.14
 btree::map::iter_100                   2,706        3,294                   588   21.73%   x 0.82
 btree::map::iteration_mut_1000         3,916        4,215                   299    7.64%   x 0.93
 btree::map::iteration_mut_100000       458,680      487,070              28,390    6.19%   x 0.94
 btree::map::range_included_excluded    390,220      410,035              19,815    5.08%   x 0.95
 btree::map::range_included_included    434,763      397,835             -36,928   -8.49%   x 1.09
 btree::map::range_unbounded_unbounded  28,255       36,724                8,469   29.97%   x 0.77
 btree::map::range_unbounded_vs_iter    28,810       34,102                5,292   18.37%   x 0.84
 btree::set::is_subset_100_vs_10k       1,269        1,394                   125    9.85%   x 0.91

This compares the best over 10 runs each. Still not particularly clear, but not quite hopeful. Using get_unchecked actually makes it seem worse.

@ssomers
Copy link
Contributor Author

ssomers commented Jul 3, 2020

Your "only way to do this currently" works too, and says basically no change in performance: benchcmp old newest --threshold 5

 name                                           old ns/iter  newest ns/iter  diff ns/iter   diff %  speedup
 btree::map::find_rand_100                      17           18                         1    5.88%   x 0.94
 btree::map::first_and_last_100                 47           42                        -5  -10.64%   x 1.12
 btree::map::first_and_last_10k                 64           74                        10   15.62%   x 0.86
 btree::map::insert_rand_100                    41           36                        -5  -12.20%   x 1.14
 btree::map::insert_rand_10_000                 41           36                        -5  -12.20%   x 1.14
 btree::map::insert_seq_100                     49           45                        -4   -8.16%   x 1.09
 btree::map::insert_seq_10_000                  94           99                         5    5.32%   x 0.95
 btree::map::range_included_excluded            390,220      410,650               20,430    5.24%   x 0.95
 btree::map::range_unbounded_unbounded          28,255       37,597                 9,342   33.06%   x 0.75
 btree::set::difference_random_100_vs_10k       2,521        2,745                    224    8.89%   x 0.92
 btree::set::intersection_random_100_vs_10k     2,322        2,581                    259   11.15%   x 0.90
 btree::set::intersection_random_10k_vs_100     2,305        2,649                    344   14.92%   x 0.87
 btree::set::intersection_staggered_100_vs_10k  2,125        2,233                    108    5.08%   x 0.95

Thanks to baby steps, I think I understand that code better than anything before, but I think you'll agree it's not great to have (more of) this sort of code written out in btree directly. But I couldn't even write a generic function reusable for keys and vals.

@ssomers ssomers changed the title BTreeMap::iter_mut and alies should not create slices every iteration BTreeMap::iter_mut and alies should not refer to slices every iteration Jul 3, 2020
@RalfJung
Copy link
Member

RalfJung commented Jul 3, 2020

Btw, what are "alies" (in the PR title)? Probably you meant "allies" but even then I don't get it.^^

@RalfJung
Copy link
Member

RalfJung commented Jul 3, 2020

Thanks to baby steps, I think I understand that code better than anything before, but I think you'll agree it's not great to have (more of) this sort of code written out in btree directly. But I couldn't even write a generic function reusable for keys and vals.

Indeed, what we should have is #60639 so you can just index the raw slice directly. #73987 makes that hard though.

@ssomers ssomers changed the title BTreeMap::iter_mut and alies should not refer to slices every iteration BTreeMap::iter_mut and allies should not refer to slices every iteration Jul 3, 2020
@ssomers
Copy link
Contributor Author

ssomers commented Jul 3, 2020

Good question, who are these allies? I was thinking of values_mut, range_mut passes through the mutable next_unchecked too. So does remove_kv_tracking but it discards the result, so I think that's all.

Which takes us back to your comment "Doing the descend [...] invalidates the references returned by into_kv_mut" now residing in that mutable next_unchecked. I wonder if it's still relevant; in fact, I wonder if that ever was enough of a solution? If the result of into_kv_mut is wrecked by navigating the tree, how can you navigate the tree while the caller can cling onto the result of into_kv_mut? Except that there was no test case clinging on to the result. Back in #58431 you wrote that len was the culprit already, and now that len is trouble free. I tried reverting that special case and Miri says it's fine now.

@ssomers ssomers changed the title BTreeMap::iter_mut and allies should not refer to slices every iteration BTreeMap mutable iterators should not take any reference to visited nodes during iteration Jul 3, 2020
@ssomers
Copy link
Contributor Author

ssomers commented Jul 3, 2020

Sigh... I just checked if "Miri is too slow" to check 144 references, i.e. 3 levels. It is not. It is, in fact, very fast to point out that something is still broken.

It's not due to the last "Try disturbing the past" commit. It happens if test_values_mut operates on 19 or more elements.

I have no idea how to figure out what Miri is upset about. The 19th element, for inserts with increasing values and if I'm not mistaken, causes the 2nd child node to split, leaving two child nodes with 6 elements each, the last child node with 5 elements, and the root 2 elements.

@ssomers
Copy link
Contributor Author

ssomers commented Jul 4, 2020

About the original code:

  • MaybeUninit::get_mut(&mut (*leaf).vals[idx]) is the code without raw pointers that keeps Miri happy on the new test (commit 5500b31b28e81b6e8f46ae19d4c50a8d2be10b86)
  • MaybeUninit::get_mut(&mut (((*leaf).vals)[idx])) works like the above (I'm not sure it's the same though)
  • ((*leaf).vals).get_unchecked_mut(idx).get_mut() compiles & tests fine but fails under Miri (didn't manage to compile with an explicit MaybeUninit::get_mut)
  • ((*leaf).vals).get_mut(idx).unwrap().get_mut() fails like the above

@ssomers
Copy link
Contributor Author

ssomers commented Jul 4, 2020

The way I understand the issue now, it seems very likely this would fail for any other mutable iterator implementation that doesn't apply social distancing to its elements, so everything but linked lists. It's definitely not hard to make Miri mad at VecDeque::iter_mut.

@RalfJung
Copy link
Member

RalfJung commented Jul 4, 2020

Except that there was no test case clinging on to the result. Back in #58431 you wrote that len was the culprit already, and now that len is trouble free. I tried reverting that special case and Miri says it's fine now.

Which special case? Fixing len just falls out of making as_leaf return a raw pointer instead of a reference, doesn't it?

MaybeUninit::get_mut(&mut (*leaf).vals[idx]) is the code without raw pointers that keeps Miri happy on the new test (commit 5500b31)

The crucial part is that it is without references: the only reference being created here is to (*leaf).vals[idx], which does not overlap with the other elements. That's why this is okay from an aliasing perspective.

&mut (((*leaf).vals)[idx])

This is the same.

The other two create a reference to the entire vals array, which overlaps with the mutable reference that was handed out earlier. This I would indeed expect them to fail.

It's definitely not hard to make Miri mad at VecDeque::iter_mut.

Uh-oh.^^ I was going to apply that mutable-iterator testcase to other collections, didn't expect issues to show up so quickly though. Do you want to open an issue for VecDeque?

@ssomers
Copy link
Contributor Author

ssomers commented Jul 4, 2020

Which special case?

The one I thought I had reverted by commit aa0681fef26beb812884de5f0a92b6f5aab4a006... but looking closer, this doesn't revert the order you set in in #58431, but merely stores the result of navigation later and obfuscates the important order. When I try to really swap the order, Miri complains again (on the 12 element test that's currently committed).

@ssomers
Copy link
Contributor Author

ssomers commented Jul 4, 2020

open an issue for VecDeque?

Sure, you'll soon get #74029 on your plate.

@RalfJung
Copy link
Member

RalfJung commented Jul 4, 2020

So is MIN_INSERTS_HEIGHT_2 still broken or did you latest changes fix that?

@ssomers
Copy link
Contributor Author

ssomers commented Jul 5, 2020

It's still broken from 19 elements on. I only changed the code backing immutable iterators.

@RalfJung
Copy link
Member

RalfJung commented Jul 5, 2020

The original mutable iterator testcase failed for 2 elements, so something presumably also changed there?

Do you know what is the problem with 19 elements or do you want me to take a look?

@ssomers
Copy link
Contributor Author

ssomers commented Jul 5, 2020

so something presumably also changed there?

Well, what changed is most of this PR? You handled len and other code using as_leaf, into_key_val_mut is also completely using raw pointers, but it's still not enough for 19 or more elements (*). This PR also tweaks the API used by immutable iterators to keep it in line with the API for mutable iterators.

No, I have no idea what the problem is and I see you write it's quite awful to debug so definitely have a look.

(*) The threshold is 19 elements if inserted in ascending order, like the test case does. In addition, one can insert up to 5 more smaller (negative) keys (which should land in the first child node).

@ssomers
Copy link
Contributor Author

ssomers commented Jul 5, 2020

I think I got there through reasoning: 19 elements is indeed the first tree with children and two elements at the root and the first tree where next_kv ascends back into the root, while there is an outstanding reference inside the root already. I bet it's actually forget_node_type that "refers to" the root node again.

@crlf0710 crlf0710 added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 5, 2020
@bors
Copy link
Contributor

bors commented Sep 5, 2020

☔ The latest upstream changes (presumably #76217) made this pull request unmergeable. Please resolve the merge conflicts.

@Mark-Simulacrum
Copy link
Member

Rebased and reviewed locally -- this seems good to me. I didn't re-run tests or miri tests, though -- will wait on PR CI to pass, but r=me with that done.

@RalfJung
Copy link
Member

RalfJung commented Sep 9, 2020

Looks like this PR does not add the Miri testcase from #73915, but I can do that in a follow-up PR (after verifying that it indeed works in Miri).

@RalfJung
Copy link
Member

RalfJung commented Sep 9, 2020

Oh never mind, this removes the #[cfg_attr(miri, ignore)], so Miri will pick it up. (Though currently there's another test suite failure so I'll have to specifically check locally.)

I'll submit a test based on test_all_refs. I thought one of @ssomers's PRs added that but I cannot find that right now?

@Mark-Simulacrum
Copy link
Member

@bors r+

@bors
Copy link
Contributor

bors commented Sep 9, 2020

📌 Commit 8158d56 has been approved by Mark-Simulacrum

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 9, 2020
@bors
Copy link
Contributor

bors commented Sep 9, 2020

⌛ Testing commit 8158d56 with merge d92155b...

@bors
Copy link
Contributor

bors commented Sep 9, 2020

☀️ Test successful - checks-actions, checks-azure
Approved by: Mark-Simulacrum
Pushing d92155b to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Sep 9, 2020
@bors bors merged commit d92155b into rust-lang:master Sep 9, 2020
@rustbot rustbot added this to the 1.48.0 milestone Sep 9, 2020
@ssomers
Copy link
Contributor Author

ssomers commented Sep 9, 2020

I'll submit a test based on test_all_refs. I thought one of @ssomers's PRs added that

test_values_mut is still there, since #74666, and now Miri knows about it.

@ssomers ssomers deleted the slice_slasher branch September 9, 2020 22:14
@RalfJung
Copy link
Member

RalfJung commented Sep 9, 2020

Oh, those tests already landed, great. :)

bors added a commit to rust-lang-ci/rust that referenced this pull request Sep 10, 2020
…ark-Simulacrum

BTreeMap: move up reference to map's root from NodeRef

Since the introduction of `NodeRef` years ago, it also contained a mutable reference to the owner of the root node of the tree (somewhat disguised as *const). Its intent is to be used only when the rest of the `NodeRef` is no longer needed. Moving this to where it's actually used, thought me 2 things:
- Some sort of "postponed mutable reference" is required in most places that it is/was used, and that's exactly where we also need to store a reference to the length (number of elements) of the tree, for the same reason. The length reference can be a normal reference, because the tree code does not care about tree length (just length per node).
- It's downright obfuscation in `from_sorted_iter` (transplanted to rust-lang#75329)
- It's one of the reasons for the scary notice on `reborrow_mut`, the other one being addressed in rust-lang#73971.

This does repeat the raw pointer code in a few places, but it could be bundled up with the length reference.

r? `@Mark-Simulacrum`
@Mark-Simulacrum
Copy link
Member

This looks like it was a small performance improvement on token-stream-stress-check of around 1.2%. Not really expected but nice anyway :)

bors added a commit to rust-lang-ci/rust that referenced this pull request Sep 18, 2020
…r=Mark-Simulacrum

BTreeMap: avoid slices even more

Epilogue to rust-lang#73971: it seems the compiler is unable to realize that creating a slice and `get_unchecked`-ing one element is a simple fetch. So try to spell it out for the only remaining but often invoked case.

Also, the previous code doesn't seem fair game to me, using `get_unchecked` to reach beyond the end of a slice. Although the local function `slice_insert` also does that.

r? `@Mark-Simulacrum`
bors added a commit to rust-lang-ci/rust that referenced this pull request Jan 26, 2021
…=Mark-Simulacrum

BTreeMap: bring back the key slice for immutable lookup

Pave the way for binary search, by reverting a bit of rust-lang#73971, which banned `keys` for misbehaving while it was defined for every `BorrowType`. Adding some `debug_assert`s along the way.

r? `@Mark-Simulacrum`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged-by-bors This PR was explicitly merged by bors. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Miri failure in btree::map::test_iter_min_max test
7 participants