Skip to content

allocator: refactor for stabilisation#157428

Open
nia-e wants to merge 20 commits into
rust-lang:mainfrom
nia-e:allocator-refactor
Open

allocator: refactor for stabilisation#157428
nia-e wants to merge 20 commits into
rust-lang:mainfrom
nia-e:allocator-refactor

Conversation

@nia-e

@nia-e nia-e commented Jun 4, 2026

Copy link
Copy Markdown
Member

View all comments

Adds my current proposal per the doc in #156882 and follow-up Zulip conversations (notably for dyn-compat) unstably.

r? libs

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jun 4, 2026
@nia-e nia-e added the A-allocators Area: Custom and system allocators label Jun 4, 2026
Comment thread library/core/src/alloc/mod.rs Outdated
@rust-log-analyzer

This comment has been minimized.

qaijuang

This comment was marked as resolved.

@rust-log-analyzer

This comment has been minimized.

@nia-e

nia-e commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Note that the no-panic bounds introduced close #156490 and #155746. However, if we want to relax them in the future, we may need to adjust our collection types to be more resilient.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

Comment thread library/alloc/src/str.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment on lines +97 to +98
/// - the allocator is mutated through public API taking `&mut` access (notably,
/// running the allocator's destructor is such a mutation), or

@clarfonthey clarfonthey Jun 4, 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.

This guarantee seems fine on the surface, but I'm trying to wrap my head around what's actually being guaranteed here. Like, clearly, it'd be wildly unsafe to offer an invalidate_everything method on an allocator that just deletes the backing memory without requiring any of the things that are using it to be dropped, but this feels like it's opening the door for that kind of method "as long as you're careful" which, doesn't make a lot of sense.

Like, I'm trying to gauge what value is being gained by this guarantee and it mostly just feels like it's making things more confusing without actually helping.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we're saying that such an invalidate_everything method is allowed to exist, and you can't rely on the allocator having not yet been dropped for soundness. in other words, so long as you hold a &alloc (thus preventing a &mut alloc from being created), you can trust the memory you have is fine; but if you lose the &alloc and get a new one back, your memory might have been scribbled over and you must act as such

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.

Okay, but wouldn't that be the same thing as the lifetime expiring as before? Technically, even though both of them are written as &A, you've gotten a new &A lifetime in that case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i think the extra guarantee here is that if you do hold a &mut alloc, you can call methods that take alloc by-shared-ref without worry but you can't pass the actual &mut to an untrusted function and expect your allocator to be okay at the end. but i agree that's not obviously guaranteed

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.

Isn't that just totally breaking the aliasing guarantees, though? Since that &mut reference wouldn't be unique.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

...you know, you make a good point. i'll revisit the reasoning for this, i recall adding this in response to something being brought up

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nvm, i'm being stupid. the following is the reason:

let mut alloc = SomeAllocator::new();
let ptr = alloc.allocate(...);
alloc.something_by_shared_ref();
// ptr is still guaranteed to be valid
alloc.trusted_method_by_unique_ref();
// ptr is still valid because we know for sure the method is trusted not to mess w/ allocator state
alloc.untrusted_method_by_unique_ref();
// ptr must be assumed to be maybe-invalid even if the lifetime of alloc is not expired and ptr hasn't yet been deallocated

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 I was technically thinking of Box whose lifecycle is intrinsically tied to the lifetime of the allocator parameter, whereas in this case if you just call alloc and dealloc manually the "lifetime" is not really tracked at all. So, yes, mutable borrows can happen on the allocator and it's fine, and you guarantee this doesn't happen by taking a non-mutable borrow.

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.

Seeing this thread from @RalfJung: #157428 (comment)

I think we probably also need to be careful about how we define the relationship between these rules and StaticAllocator, since "lifetime expiration" in those cases refers to the allocator value and not references in that case.

Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
Comment thread library/core/src/alloc/mod.rs Outdated
@clarfonthey

Copy link
Copy Markdown
Contributor

@rustbot author

(mostly so you can more clearly signal when you think things are ready; I've commented here already so I'll see any additional changes for review as they're made)

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 4, 2026
///
/// [`Pin`]: ../../core/pin/struct.Pin.html
#[unstable(feature = "allocator_api", issue = "32838")]
pub unsafe trait StaticAllocator: Allocator {}

@nia-e nia-e Jun 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I dropped the 'static bound here (and thus implicitly in Box::pin_in, etc.); since this trait is about being able to be lifetime-subtyped safely, it would mean that you need to be able to coerce to a StaticAllocator + 'a so the whole guarantee about "this is Actually Static I Promise" has weight. cc @rust-lang/opsem in case i did a bad here

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's more of a @rust-lang/types question

@rust-log-analyzer

This comment has been minimized.

@maxdexh

maxdexh commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

I would be more scared about some user assuming that when they have T: UnsizeCoerced<U>, that that generically means that they get an equivalent allocator out of it, than of allocators unsize coercing to non-equivalent ones. If a smart pointer wants to do that, they probably already can't implement Allocator soundly. Why are we making a guarantee for consumers of the allocator api, when you'd never want to unsize coerce an allocator generically (you can't even atm without the CoerceUnsized trait). Am I missing something here?

@maxdexh

maxdexh commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Same thing for trait upcasting. In my mind, trait upcasting preserves equivalence as a result of an invariant of the container type stating that you keep the underlying object. This implies equivalence by reflexivity, rather than as an invariant of Allocator.

Comment thread library/alloc/src/str.rs
#[unstable(feature = "allocator_api", issue = "32838")]
#[must_use]
#[inline]
pub unsafe fn from_boxed_utf8_unchecked_in<A: crate::alloc::Allocator>(

@clarfonthey clarfonthey Jul 20, 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.

Not to nitpick too strongly but generally I would expect the _in methods to take an allocator separately, not just apply to an existing allocation with an allocator already bundled with it.

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i don't mind changing the name or even making this private. i just needed the method for that one conversion /shrug

@@ -345,48 +345,3 @@ fn issue_158875_make_mut_dont_leak_allocator() {

assert_eq!(Rc::strong_count(&alloc), 1); // if this is >1, we have a memory leak!
}

/// Test that `Arc::make_mut` does not cause a UAF if the allocator panics on

@clarfonthey clarfonthey Jul 20, 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.

The main reason for removing this is that this should just abort now, right?

View changes since the review

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.

Yes (I added this test)

@maxdexh maxdexh Jul 20, 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.

Oh sorry, no this should not abort. The code doesn't compile because of a missing AllocatorClone bound. With it, the impl has UB because you must not unwind

Comment thread library/std/src/alloc.rs
@@ -139,6 +139,12 @@ use crate::{hint, mem, ptr};
#[derive(Debug, Default, Copy, Clone)]
pub struct System;

@clarfonthey clarfonthey Jul 20, 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.

Shouldn't System be NoaliasAllocator too?

View changes since the review

@maxdexh maxdexh Jul 20, 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.

No. As discussed on zulip, the system allocator on windows does not meet the requirements.

Edit: Fixed link to point at the right comment

@RalfJung

Copy link
Copy Markdown
Member

i am going to go on a whim and say we're probably not stabilising ppl manually implementing that any time soon. additionally, even if we do, it'd be a very compiler-internal-magic trait and i can't imagine that changing; the meaning of coerceunsized is "give me an identical thing, but make it ?Sized". i guess cc @RalfJung on behalf of opsem here? can we say in the safety docs of Allocator that CoerceUnsized must be well-behaved if implemented (i.e. allocator equivalence cannot be broken by an unsize coercion)?

I'm not an expert for that trait, I don't really have intuition for it. This is more of a case for @rust-lang/types @Darksonn .

Comment thread library/core/src/alloc/mod.rs Outdated
Comment on lines +554 to +555
/// This is *highly unlikely* to be possible to implement for most allocators. LLVM
/// maintains special-case code for the global allocator in order to enable this trait

@RalfJung RalfJung Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure what LLVM special-case code you are referring to here.

Also I think we are mixing up two aspects of allocators, but maybe that's okay:

  • Box having noalias
  • calls to the allocator methods being considered native allocation functions by LLVM (this is where LLVM has special-case code)

It seems useful to let custom allocators opt-in to the latter. The consequences of that go a bit further than what this comment says though, e.g. this gives LLVM license to entirely omit matching alloc/free calls. Also if we do apply the native allocator treatment to custom allocators then I think we can even mark Windows System as such, i.e. native allocator treatment helps work around the annoying noalias problems.

I assume that this trait is not part of the initial stabilization, so we don't have to figure this all out now. The native allocator treatment in LLVM is also currently being refined and fixed so we'd anyway want to wait for that to settle before committing to anything here.

The one thing we probably should decide before stabilization is if we want to reserve the right to mark all custom allocators as native allocation functions in LLVM, or if we want to reserve those optimizations to the allocators that opt-in to them. The way LLVM (and also Miri FWIW) model native allocator semantics means that we probably have to wrap the calls to allocator methods in, well, wrappers that we decorate with extra attributes and than contain some magic intrinsics. So I could also imagine that we end up with a

struct NativeAllocator<A: Allocator>(A);

such that NativeAllocator<MyAllocator> is a version of MyAllocator that has the requirements and optimizations of an LLVM native allocator. And then NativeAllocator (and Global which would implicitly add the same wrapper/intrinsics) would be the key to control noalias I guess? If we go with that plan the main remaining thing that is unclear to me is whether we can wrap any allocator in NativeAllocator or whether the allocator also needs to satisfy some extra contract for that. I think every allocator should be fine honestly. But if we discover that that's not the case we can always add a trait.

Uh sorry this ended up being more rambling than a coherent comment. I hope it's still useful.^^

View changes since the review

@nia-e nia-e Jul 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

AIUI, Box's noalias depends on LLVM's special-casing of native allocation functions to be sound. The intent behind NoaliasAllocator was to enable noalias on Box without said special-case code being necessary, so per your proposal a NativeAllocator<A: NoaliasAllocator> should be able to behave identically to the bare underlying A.

e.g. this gives LLVM license to entirely omit matching alloc/free calls

This is already half-mentioned in the safety comment on GlobalAllocator itself:

You may generally not rely on heap allocations happening if they can be removed without changing program behavior.

(though this only allows allocation calls to be removed, not also added; I'm happy to tighten that though). If we can mark all allocators as native through such a wrapper, that would be great, but I don't think it's strictly mandatory and I'm okay adding a different trait for that even if that would be non-ideal. unsure who needs to be cc'd here to confirm the current requirements are sufficient, or whether that's even possible to confirm before llvm decides on something though

@RalfJung RalfJung Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AIUI, Box's noalias depends on LLVM's special-casing of native allocation functions to be sound.

I think it mostly depends on LLVM's not ever inlining native allocation functions to be sound. ;)
LLVM is currently discussing a model for allocators that can survive inlining. That requires putting special intrinsics into their body, hence the "wrapper" I mentioned.

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.

You may generally not rely on heap allocations happening if they can be removed without changing program behavior.

That's from GlobalAllocator, not Allocator.

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.

Allocator has another such section, but it is even less strict.

Comment thread library/core/src/alloc/mod.rs Outdated
/// aliasing rules for mutable borrows apply: when their lifetime ends (e.g. because a
/// pointer they were derived from gets used again), they must not be used anymore.
///
/// This is *highly unlikely* to be possible to implement for most allocators. LLVM

@maxdexh maxdexh Jul 20, 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.

Is it so unlikely? All you need to do to meet these requirements is store the metadata separately. I feel like this statement only applies to global allocators, whereas many non-global allocators can implement this, e.g. bumpalo::Bump.

View changes since the review

@RalfJung RalfJung Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we also use this for LLVM native allocator support then another requirement is that the allocation will not be freed by any means other than calling free. That would exclude bump allocators that free their arena on drop.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

cc @thomcc, but as I understand it yes. either way, I think it's better to dissuade ppl here and this would mostly just be a blocker for stabilisation of NoaliasAllocator

@maxdexh maxdexh Jul 20, 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.

If we also use this for LLVM native allocator support then another requirement is that the allocation will not be freed by any means other than calling free.

If we go with those requirements, then this is essentially a subtrait of StaticAllocator

@RalfJung RalfJung Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You may generally not rely on heap allocations happening if they can be removed without changing program behavior.

I don't think that wording achieves what you want. This wording is a complete NOP, the compiler can already remove any call if that can be done without changing program behavior. If the allocator contains a println!, removing it changes program behavior.

The point of the magic allocator optimizations (that we don't currently apply to custom allocators, only to global allocators) is that allocations can be removed even if that changes program behavior.

View changes since the review

@maxdexh maxdexh Jul 20, 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.

The next sentence clarifies that it means even if you print in the allocator, removing the call is not considered changing program behavior. I think this is just confusingly worded.

@nia-e nia-e Jul 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

tbh, I'm in favour of making GlobalAllocator a much looser marker trait on top of (Static?)Allocator. The current safety requirements on it are never unwinding (which we now will require on all allocators as of this PR), the bit about allocations being considered non-observable (which i'm happy to move to Allocator itself in full), and a promise about reentrancy in std. only the last of these is actually safety-relevant, and right now we don't even care about that safety promise (we assume all global allocators are maybe-reentrant). libs-api consensus was to add a const bool to GlobalAllocator for whether it's reentrant, which would always be okay to set to true. that way, NoaliasAllocator can just mean the thing on the tin, "you can emit noalias on Box when we use this allocator", and Global becomes a wrapper along the lines of what Ralf suggested earlier. that is, the line of requirements is:

trait Allocator {} // Allocations are always non-observable, etc
trait StaticAllocator: Allocator {} // Only explicit `deallocate()` ever frees memory (no `Drop`-freeing, etc)
trait NoaliasAllocator: StaticAllocator {} // Box<T, A: NoaliasAllocator> can emit noalias
trait GlobalAllocator: StaticAllocator + Sync + 'static { const REENTRANT_IN_STD: bool; } // Safe to implement, except that constant must be correct

#[lang = "native_allocator_magic"]
struct NativeAllocator<A: StaticAllocator>(A);
impl<A: StaticAllocator> NoaliasAllocator for NativeAllocator<A> {}
type Global = NativeAllocator</* magic idk */>;

@maxdexh maxdexh Jul 20, 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.

Any allocator stored in a static is effectively StaticAllocator, since it must be 'static and therefore has no lifetimes that can expire, cannot be dropped, and has no mutable API. Therefore it is not possible for all equivalent versions of it to ... (like before with forget)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If NoaliasAllocator is just about noalias then I don't think it relates to StaticAllocator.

But I don't think we should have a trait that's just for noalias. I think we should just have one trait that makes the allocator "magic that is understood by the compiler", and that should enable noalias on Box, unique ownership of alloc return value, allocation elision, etc. Some of those constrain the allocator, some the user, but I don't see a good reason to split them up. Or alternatively one magic wrapper type that gives these semantics, and then NativeAllocator is the only one that gets noalias too.

@nia-e nia-e Jul 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

that would need name bikeshedding, but yeah i agree. if we can just do a wrapper type around StaticAllocator and entirely ditch the trait, that makes it even easier imo.

either way, it seems like that's an open question and it's non-blocking for stabilising Allocator, so I'll just remove it for now; I'll open a zulip thread about how we want to handle allocators that obey llvm native allocator semantics (in t-opsem I suppose?)

@maxdexh

maxdexh commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

I think we may have too many allocator traits.

The requirements are starting to blend together as well.

It feels more like each of them is an ingredient in a cooking recipe.

@nia-e

nia-e commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

yeah 😭 though i'm not sure we can support everyone's usecases without a bunch of traits. as I see it, per my last comment:

  • Allocator without StaticAllocator is needed to support stack arenas, bump allocators, etc.;
  • StaticAllocator without NoaliasAllocator is needed to support... the vast majority of global-ish allocators that aren't shelling out to FFI, tbh (linked list or mmap-based allocators, for instance; anything that doesn't have a clear unique source of truth for where its allocation state metadata lives)
  • GlobalAllocator is needed for the reentrancy promises
  • NoaliasAllocator is needed because lang wants noalias on Box and that's a battle better fought separately since we're not stabilising that now anyways

and I don't think we can shorten it past this without cutting into usecases we want to support

@nia-e

nia-e commented Jul 20, 2026

Copy link
Copy Markdown
Member Author

(tbh, I'm also happy axing NoaliasAllocator for now and replacing it with Ralf's suggestion of a wrapper type - either way that one would stay unstable for a loooong time probably)

@maxdexh

maxdexh commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
image

"Let me add just one more trait"

Jokes aside, I agree. The docs of std::alloc can be written to point people to the ones that matter, while mentioning the more niche ones at the side.


/// Marks a type's [`PartialEq`] implementation as sound with regard to [`Allocator`].
/// Implementors must ensure that, upon equality, the two allocators are interchangeable
/// (i.e. it is possible to free memory with one that was allocated with the other), and

@maxdexh maxdexh Jul 20, 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.

This should just say "are equivalent" and link to the respective Allocator section.

View changes since the review

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.

Same thing for AllocatorClone

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i left the bigger explanation there so it's conveniently readable, but i added the necessary Word so ppl can check easily

@maxdexh maxdexh Jul 21, 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.

To nit a bit more, the "i.e." should imo be an "in particular", since this is not the full meaning of equivalence.

It's also weird that AllocatorClone doesn't have a safety section.

@theemathas

Copy link
Copy Markdown
Contributor

The allocators in BTreeMap/BTreeSet are currently bound by Allocator + Clone. Do those need to be bound with AllocatorClone?

@nia-e

nia-e commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

yes, but that's not blocking rn imo since btree + custom allocator (a) will take significantly more work, (b) btreemap/set are also used in rustc_data_structures so migrating them is also a compiler change and should be its own separate pr imo

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

It's a different feature gate (btreemap_alloc) and probably needs more work, since it's cloning around the allocator everywhere, which is really awkward with Box<A> and also susceptible to panics on allocator drop (see my issues).

@RalfJung

RalfJung commented Jul 21, 2026

Copy link
Copy Markdown
Member

It's a different feature gate (btreemap_alloc) and probably needs more work, since it's cloning around the allocator everywhere, which is really awkward with Box<A> and also susceptible to panics on allocator drop (see my issues).

It used to instead put an &A everywhere which IMO is worse as it is not zero-cost for A = Global.
I still think we should remove the impl<A: Allocator> Allocator for &A as it invites exactly those kinds of mistakes...

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Cloning a Box<A> is (checks notes) also not quite zero-cost 😅

I think we'll need a middle ground here, like some extra abstraction that allows allocators to specify how they would like to be passed around. E.g.

unsafe trait AllocatorUse { 
    type Use<'a>: Allocator + 'a where Self: 'a; 
    fn use_allocator(&self) -> Self::Use<'_>;
}

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

If it fails to optimize away the &Global, then I also doubt it will be able to optimize the indirection in &Arc<A> and similar. Such an API would allow us to state that Arc::<A>::Use = A::Use. I'll make a zulip topic.

@RalfJung

RalfJung commented Jul 21, 2026

Copy link
Copy Markdown
Member

Cloning a Box is (checks notes) also not quite zero-cost 😅

Cloning Global is, though, and that's a more important allocator to optimize for if we can't have our cake and eat it too. ;)
The unstable feature was causing perf regressions for stable code, which had to stop even if it resulted in bad performance for unstable code, IMO.

If it fails to optimize away the &Global

That's not an optimization, it's a change in program behavior. &Global is 8 bytes in size, and we specify that those 8 bytes are actually carried around, making the BTree types 8 bytes bigger than they have to be.

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The only types that actually carried it around were Entry variants. Most clones atm are just being passed as function arguments.

I also disagree with the sentiment that Global is more important. This is true right now, but when Allocator becomes stable, cloning will also result in unnecessarily slow, stable code.

@RalfJung

RalfJung commented Jul 21, 2026

Copy link
Copy Markdown
Member

That's still passing things around, wastefully using up precious registers.
To be fair I never benchmarked this, but it did seem pretty unacceptable to me.

I also disagree with the sentiment that Global is more important. This is true right now, but when Allocator becomes stable, cloning will also result in unnecessarily slow, stable code.

That's new code though. You can't regress all existing code using btrees for the sake of new code. Also I am quite sure Global is going to remain at least 2 orders of magnitude more widely used than any other allocator, so it will remain vastly more important.

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

I agree, hence why I suggested something like AllocatorUse. For Global, we can pass around a copy (even Arc<Global> would be passed as Global). I think it is not acceptable to clone Box<A> O(logn) times unnecessarily.

@nia-e

nia-e commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

i mean, it's not a huge issue if we ditch the impl on references. i'd still keep around e.g. AllocatorClone for &A, just with the requisite where-bounds (where A: Allocator, &A: Allocator), so if someone does manually impl Allocator for both, they can use apis bounded by unstable traits. and i agree with ralf that regressing safe code is a nonstarter here

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Have fun rewriting the internals of Arc and Rc ^^
This will also cause panic safety issues over there with panicking drops.

@RalfJung

Copy link
Copy Markdown
Member

Have fun rewriting the internals of Arc and Rc

Where/how do those rely on the &A impl?

@maxdexh

maxdexh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Discussion on zulip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-allocators Area: Custom and system allocators S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet