Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions library/alloc/src/collections/btree/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,17 @@ pub struct BTreeMap<
#[stable(feature = "btree_drop", since = "1.7.0")]
unsafe impl<#[may_dangle] K, #[may_dangle] V, A: AllocatorClone> Drop for BTreeMap<K, V, A> {
fn drop(&mut self) {
// ignore-tidy-undocumented-unsafe
drop(unsafe { ptr::read(self) }.into_iter())
// Skip `into_iter` for an empty map: `dying_next` is too costly to inline, so the
// empty drop isn't optimised away (see #161375).
if self.root.is_some() {
// SAFETY: `self` is not used after this and none of its fields are dropped again:
// `alloc` is `ManuallyDrop` and `root` has no drop glue.
drop(unsafe { ptr::read(self) }.into_iter())
Comment on lines +211 to +213

@clarfonthey clarfonthey Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that part of my confusion here is why this isn't sufficiently optimized into a no-op for an empty tree, and hence needed at all.

I figure it makes sense just due to the combined complexity of the function, but is there maybe other detail we could add here to make that clear, so maybe this could be fixed later?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so at -Copt-level=3 inlining doesn’t occur because it’s too costly to inline (cost=850, threshold=250), and setting inline-threshold to 2000 does inline to just ret void.

I’ll add comment to explain this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would inline(always) on some of the internal methods make the difference?

Mostly trying to see if we can potentially circumvent the need for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding inline(always) on dying_next does fix the empty drop without the root check.

But the catch is that IntoIter::next uses dying_next too, so it gets inlined there as well.

With a small test:

use std::collections::BTreeMap;
#[no_mangle]
pub fn sum(m: BTreeMap<u64, u64>) -> u64 { 
    m.into_iter().map(|(k, v)| k + v).sum() 
}

At -Copt-level=3 this function grows from 165 to 429 lines of IR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, seems definitely not worth it. Thank you for the extra research; I figured this was the right choice, just, wanted to make extra sure.

} else {
// SAFETY: With no root there are no nodes to free, so only the allocator needs
// dropping. `self` is not used after this, and `alloc` is dropped only here.
unsafe { ManuallyDrop::drop(&mut self.alloc) }
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions tests/codegen-llvm/btree-empty-drop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Regression test for https://github.com/rust-lang/rust/issues/161375
// Checking that an empty BTree's drop is optimized away.
//@ compile-flags: -Copt-level=3

#![crate_type = "lib"]

use std::collections::BTreeMap;

// CHECK-LABEL: @drop_btree(
// CHECK-NEXT: {{.*}}:
// CHECK-NEXT: ret void
#[no_mangle]
pub fn drop_btree() {
let _ = BTreeMap::<(), ()>::new();
}
Loading