Repository navigation
BTreeMap::merge optimized - #152418
BTreeMap::merge optimized#152418
BTreeMap::merge optimized#152418Conversation
a5a105e to
a7fa7dd
Compare
| // SAFETY: We read in self_val's and hand it over to our conflict function | ||
| // which will always return a value that we can use to overwrite what's | ||
| // in self_val | ||
| unsafe { | ||
| let val = ptr::read(self_val); | ||
| let next_val = (conflict)(self_key, val, first_other_val); | ||
| ptr::write(self_val, next_val); | ||
| } |
There was a problem hiding this comment.
this should really be a method of Cursor*, you need a handler for when conflict panics that removes the entry without dropping whatever you ptr::read from.
something like:
impl<K, V> CursorMut<'a, K, V> {
/// call `f` with the next entry's key and value, replacing the next entry's value with the returned value. if `f` unwinds, the next entry is removed.
/// equivalent to a more efficient version of:
/// ```rust
/// if let Some((k, v)) = self.remove_next() {
/// let v = f(&k, v);
/// // Safety: key is unmodified
/// unsafe { self.insert_after_unchecked(k, v) };
/// }
/// ```
pub(super) fn with_next(&mut self, f: impl FnOnce(&K, V) -> V) {
struct RemoveNextOnDrop<'a, 'b, K, V> {
cursor: &'a mut CursorMut<'b, K, V>,
forget_next: bool,
}
impl<K, V> Drop for RemoveNextOnDrop<'_, '_, K, V> {
fn drop(&mut self) {
if self.forget_next {
// call an equivalent to CursorMut::remove_next()
// except that instead of returning `V`, it never moves or drops it.
self.0.forget_next_value();
}
}
}
let mut remove_next_on_drop = RemoveNextOnDrop {
cursor: self,
forget_next: false, // we don't know that we have a next value yet
};
if let Some((k, v_mut)) = remove_next_on_drop.cursor.peek_next() {
remove_next_on_drop.forget_next = true;
// Safety: we move the V out of the next entry,
// we marked the entry's value to be forgotten
// when remove_next_on_drop is dropped that
// way we avoid returning to the caller leaving
// a moved-out invalid value if `f` unwinds.
let v = unsafe { std::ptr::read(v_mut) };
let v = f(k, v);
// Safety: move the V back into the next entry
unsafe { std::ptr::write(v_mut, v) };
remove_next_on_drop.forget_next = false;
}
}
}The equivalent CursorMutKey method should instead have f be impl FnOnce(K, V) -> (K, V) and needs to forget both the key and value since they were both ptr::read, and not just the value.
|
Appreciate your patience and feedback in all of this @programmerjake! |
|
@programmerjake Took your suggestion on Also, I've been wondering if |
| let mut emptied_internal_root = false; | ||
| if let Ok(next_kv) = current.next_kv() { | ||
| let ((_, val), pos) = | ||
| next_kv.remove_kv_tracking(|| emptied_internal_root = true, self.alloc.clone()); |
There was a problem hiding this comment.
remove_kv_tracking may panic and drop the key and value, tbh this is very complicated and makes me think it could be better for with_next() instead of calling forget_next[_key_and]_value, to transmute the cursor reference type from &mut CursorMut<K, V> to &mut CursorMut<K, ManuallyDrop<V>> and then just call remove_next. that said, idk if that would be sound, asking on Zulip: #t-libs > could we use transmute tricks in the impl of BTreeMap? @ 💬
There was a problem hiding this comment.
True, I think transmute should be fine since &mut CursorMut<K, V> and &mut CursorMut<K, ManuallyDrop<V>> are the same size and it forgets the original, so we wouldn't be running the destructor on the original. (but of course I can't hold myself to that since I haven't played around with mem::transmute much myself)
There was a problem hiding this comment.
it might differ with -Zrandomize-layout or other future struct layout changes. I haven't reviewed the whole BTreeMap implementation to see what layout guarantees it has.
|
Reminder, once the PR becomes ready for a review, use |
6d826f5 to
97f5547
Compare
commented
Feb 12, 2026
|
@programmerjake Made the change, lmk what you think! |
b26e4e1 to
6c436c1
Compare
6c436c1 to
9b423c5
Compare
|
The tests I took verbatim from One thing I'll point out is that interestingly this #[test]
#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")]
fn test_append_drop_leak() {
let a = CrashTestDummy::new(0);
let b = CrashTestDummy::new(1);
let c = CrashTestDummy::new(2);
let mut left = BTreeMap::new();
let mut right = BTreeMap::new();
left.insert(a.spawn(Panic::Never), ());
left.insert(b.spawn(Panic::Never), ());
left.insert(c.spawn(Panic::Never), ());
right.insert(b.spawn(Panic::InDrop), ()); // first duplicate key, dropped during append
right.insert(c.spawn(Panic::Never), ());
catch_unwind(move || left.append(&mut right)).unwrap_err();
assert_eq!(a.dropped(), 1);
assert_eq!(b.dropped(), 1); // should be 2 were it not for Rust issue #47949
assert_eq!(c.dropped(), 2);
}When I ported a similar version of this for #[test]
#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")]
fn test_merge_drop_leak() {
let a = CrashTestDummy::new(0);
let b = CrashTestDummy::new(1);
let c = CrashTestDummy::new(2);
let mut left = BTreeMap::new();
let mut right = BTreeMap::new();
left.insert(a.spawn(Panic::Never), ());
left.insert(b.spawn(Panic::Never), ());
left.insert(c.spawn(Panic::Never), ());
right.insert(b.spawn(Panic::InDrop), ()); // first duplicate key, dropped during append
right.insert(c.spawn(Panic::Never), ());
catch_unwind(move || left.merge(right, |_, _, _| ())).unwrap_err();
assert_eq!(a.dropped(), 1);
assert_eq!(b.dropped(), 2);
assert_eq!(c.dropped(), 2);
}The |
commented
Feb 14, 2026
|
I added a panic test within conflict, but I think due to #47949, it isn't incrementing the counter for |
commented
Feb 15, 2026
|
r? @jhpratt rustbot has assigned @jhpratt. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
Mark was the reviewer of my previous BTreeMap merge PR (which uses double iterator) and I recall that jhpratt mentioned he's stepping off rotation for a bit last week on the previous BTreeMap PR (not sure if it's true right now), so I'm just rolling this review to Mark. |
commented
Feb 25, 2026
commented
Feb 25, 2026
|
@rust-timer build 73143b0 For #153074. |
commented
Feb 25, 2026
|
Missing artifact for sha |
commented
Feb 25, 2026
|
@rust-timer build 73143b0 |
This comment has been minimized.
This comment has been minimized.
commented
Feb 25, 2026
|
Finished benchmarking commit (73143b0): comparison URL. Overall result: no relevant changes - no action neededBenchmarking this pull request means it may be perf-sensitive – we'll automatically label it not fit for rolling up. You can override this, but we strongly advise not to, due to possible changes in compiler perf. @bors rollup=never Instruction countThis benchmark run did not return any relevant results for this metric. Max RSS (memory usage)Results (primary 5.7%, secondary -4.0%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (secondary -2.5%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 482.035s -> 480.599s (-0.30%) |
View all comments
This is an optimized version of #151981. See ACP and tracking issue for more information on
BTreeMap::mergedoes.CC @programmerjake. Let me know what you think of how I'm using
CursorMutandIntoIterhere and whether the unsafe code here looks good. I decided to useptr::read()andptr::write()to grab the value fromCursorMutasVthan&mut V, use it within theconflictfunction, and overwrite the content of conflicting key afterward.I know this needs some polishing, especially with refactoring some redundant looking code in a nicer way, some of which could probably just be public API methods for
CursorMut. It does pass all the tests that I currently have forBTreeMap::merge(inspired fromBTreeMap::append) though, so that's good.