BTreeMap::drain_filter should not touch the root during iteration#74762
BTreeMap::drain_filter should not touch the root during iteration#74762bors merged 2 commits intorust-lang:masterfrom ssomers:btree_no_root_in_remove_kv_tracking
Conversation
|
(rust_highfive has picked a reviewer for you, use r? to override) |
|
☔ The latest upstream changes (presumably #73265) made this pull request unmergeable. Please resolve the merge conflicts. |
|
I am still not certain that the UB is there, but it seems like the related code cleanups in this PR are good regardless. @bors r+ |
|
📌 Commit 99398dd has been approved by |
|
You're probably right. I assumed that Note that the earlier commit simplified code a little more, but recent benchmarking said consistently this costs 5 to 10%, so I passed the |
Rollup of 10 pull requests Successful merges: - rust-lang#74686 (BTreeMap: remove into_slices and its unsafe block) - rust-lang#74762 (BTreeMap::drain_filter should not touch the root during iteration) - rust-lang#74781 (Clean up E0733 explanation) - rust-lang#74874 (BTreeMap: define forget_type only when relevant) - rust-lang#74974 (Make tests faster in Miri) - rust-lang#75010 (Update elasticlunr-rs and ammonia transitive deps) - rust-lang#75041 (Replaced log with tracing crate) - rust-lang#75044 (Clean up E0744 explanation) - rust-lang#75054 (Rename rustc_middle::cstore::DepKind to CrateDepKind) - rust-lang#75057 (Avoid dumping rustc invocations to stdout) Failed merges: - rust-lang#74827 (Move bulk of BTreeMap::insert method down to new method on handle) r? @ghost
…ulacrum BTreeMap: better way to postpone root access in DrainFilter A slightly more elegant (in my opinion) adaptation of rust-lang#74762. Benchmarks seem irrationally pleased to: ``` benchcmp old new --threshold 5 name old ns/iter new ns/iter diff ns/iter diff % speedup btree::map::clone_fat_100_and_remove_all 215,182 185,052 -30,130 -14.00% x 1.16 btree::map::clone_fat_100_and_remove_half 139,667 127,945 -11,722 -8.39% x 1.09 btree::map::clone_fat_val_100_and_remove_all 96,755 81,279 -15,476 -16.00% x 1.19 btree::map::clone_fat_val_100_and_remove_half 64,678 56,911 -7,767 -12.01% x 1.14 btree::map::find_rand_100 18 17 -1 -5.56% x 1.06 btree::map::first_and_last_0 33 35 2 6.06% x 0.94 btree::map::first_and_last_100 40 54 14 35.00% x 0.74 btree::map::insert_rand_100 45 42 -3 -6.67% x 1.07 btree::map::insert_rand_10_000 45 41 -4 -8.89% x 1.10 btree::map::iter_0 2,010 1,759 -251 -12.49% x 1.14 btree::map::iter_100 3,514 2,764 -750 -21.34% x 1.27 btree::map::iter_10k 4,018 3,768 -250 -6.22% x 1.07 btree::map::range_unbounded_unbounded 37,269 28,929 -8,340 -22.38% x 1.29 btree::map::range_unbounded_vs_iter 31,518 28,814 -2,704 -8.58% x 1.09 ``` r? @Mark-Simulacrum
Illustration of the alleged undefined behaviour under stacked borrows rules:Say we have a 2-level map with 11 keys -5..=5. The root has key 0, a left child with keys -5..=-1, a right child with keys 1..=5. You won't get this 2nd level inserting 11 elements from scratch, but you can get there removing an element from a larger map. We start or virtually: That's a raw pointer access (unless there's something like #73987 going on). If we turn it into a "real" |
…mulacrum Revert the fundamental changes in rust-lang#74762 and rust-lang#75257 Before possibly going over to rust-lang#75487. Also contains some added and fixed comments. r? @Mark-Simulacrum
Although Miri doesn't point it out, I believe there is undefined behaviour using
drain_filterwhen draining the 11th-last element from a tree that was larger. When this happens, the last remaining child nodes are merged, the root becomes empty and is popped from the tree. That last step establishes a mutable reference to the node elected root and writes a pointer innode::Root, while iteration continues to visit the same node.This is mostly code from #74437, slightly adapted.