[PATCH] libstdc++: Add allocate_at_least (P0401) [PR118030]
Jonathan Wakely
jwakely.gcc@gmail.com
Mon Mar 9 18:13:55 GMT 2026
On Mon, 9 Mar 2026 at 15:18, Nathan Myers <ncm@cantrip.org> wrote:
>
> On 3/9/26 6:01 AM, Jonathan Wakely wrote:
> >
> >
> > On Sun, 8 Mar 2026, 23:39 Nathan Myers, <ncm@cantrip.org
> > <mailto:ncm@cantrip.org>> wrote:
> >
> > Have you thought about how std::allocator<>::allocate_at_least
> > might usefully discover whether the ::op new (it is obliged by the
> > Standard to use) is the one in libstdc++, and will supply extra
> > details about memory allocated, not one the user has supplied and
> > the linker has substituted in its place? Ideally it would all be
> > decided at link time, and not conditionally in every call, but
> > linker magic is fragile even without LTO. And users can call ::op
> > new directly, with no #include to declare it.
> >
> > Maybe pass an extra argument to ::op new that the user's version
> > won't notice, and that ours may discover is present by looking at
> > the stack frame? The extra argument might be a pointer to a place
> > to scribble extra details. (A hint argument might be placed there,
> > besides.) But stack frame conventions vary too.
> >
> >
> > We don't need to do that. This feature is a customisation point for non-
> > standard allocators (and program-defined specializations of
> > std::allocator<User type>), we don't need to make std::allocator use
> > it. P0901 proposed changes to operator new which would have been useful
> > here, but that got abandoned.
>
> I don't understand that. If a user displaces ::operator new, they
> will expect std::allocator members to call it. The Standard seems
> to require all members obtain whatever memory they deliver by
> calling ::operator new. So, when calls to ::operator new are visible,
> we seem to be obliged to use it.
>
> > I did post a prototype in https://gcc.gnu.org/PR106477 <https://
> > gcc.gnu.org/PR106477> which can detect whether operator new has been
> > replaced, but I don't think we would need that here.
> >
> > We could just have a thread_local size_t* which is initially null and
> > which std::allocator would set to a local size_t, and operator new could
> > check for non-null and conditionally write the allocated size to it. But
> > that assumes that malloc provides a way to get the size. If a user
> > interposes their own malloc but doesn't work replace malloc_usable_size
> > then you'd have UB.
>
> Thread-local storage is a big hammer. Linker magic seems fragile.
> We could make calling ::operator new(0) poke a global if it knows
> about that global:
>
> std::atomic<int> ::__op_new_state{0};
>
> auto allocate_at_least(size_t __n) -> allocation_result<pointer>
> {
> auto __state = __op_new_state.load(memory_order_relaxed);
> if (__state > 0)
> return _M_allocate_at_least(__n);
> else if (__state < 0)
> return { ::operator new(__n * sizeof(_Tp)), __n };
>
> ::operator delete(::operator new(0)); // sample it
> if (__op_new_state.load(memory_order_relaxed) == 0) // still?
> __op_new_state.store(-1, memory_order_relaxed);
> return allocate_at_least(n);
> }
>
> void* ::operator new(size_t n)
> {
> if (n == 0 && ::__op_new_state.load(memory_order_relaxed) == 0)
> ::__op_new_state.store(1, memory_order_relaxed);
> ...
> }
>
> Relaxed semantics is safe here because all writers would write
> the same value. I don't think there is any repetitive performance
> penalty for relaxed reads to an atomic value that is not changing.
>
> Allowing the user to poke __op_new_state in their ::op new and
> supply their own __allocate_at_least to use is trivially more
> complicated.
>
> > A safer and more conservative approach would be for
> > std::allocator::allocate_at_least to just round up to a multiple of
> > alignof(max_align_t) (as long as the total number of bytes being
> > allocated is already at least alignof(max_align_t)). That should be a
> > separate change though.
>
> That seems rather pointless, though.
It's an almost zero-overhead way for std::string to get a few more
bytes of memory that malloc is already going to reserve anyway.
Likewise for std::vector<T> where T is small, like int.
It's exactly what the feature is designed for: take advantage of extra
bytes that malloc reserves anyway, but we wouldn't otherwise be able
to use.
(Admittedly, std::vector and std::string could have implemented that
logic for themselves, but putting it in
std::allocator::allocate_at_least is a better place for it, as that
puts the logic into the allocator that knows it's using something
malloc-like).
The only downside I see is that it assumes that std::allocator always
gets something from a malloc-like allocator, but if operator new has
been replaced then it's possible that the assumption about rounding up
the allocation size isn't valid.
More information about the Libstdc++
mailing list