[PATCH] libstdc++: Add allocate_at_least (P0401) [PR118030]

Jonathan Wakely jwakely.gcc@gmail.com
Tue Mar 10 13:29:44 GMT 2026


On Tue, 10 Mar 2026 at 13:13, Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>
>
>
> On Tue, Mar 10, 2026 at 2:04 PM Nathan Myers <ncm@cantrip.org> wrote:
>>
>> On 3/10/26 8:48 AM, Tomasz Kaminski wrote:
>> >
>> >
>> > On Tue, Mar 10, 2026 at 1:30 PM Nathan Myers <ncm@cantrip.org
>> > <mailto:ncm@cantrip.org>> wrote:
>> >
>> >     On 3/10/26 8:12 AM, Jonathan Wakely wrote:
>> >      > On Tue, 10 Mar 2026 at 12:05, Nathan Myers <ncm@cantrip.org
>> >     <mailto:ncm@cantrip.org>> wrote:
>> >      >>
>> >      >> On 3/10/26 7:55 AM, Tomasz Kaminski wrote:
>> >      >>>
>> >      >>>
>> >      >>> On Tue, Mar 10, 2026 at 12:17 PM Nathan Myers <ncm@cantrip.org
>> >     <mailto:ncm@cantrip.org>
>> >      >>> <mailto:ncm@cantrip.org <mailto:ncm@cantrip.org>>> wrote:
>> >      >>>
>> >      >>>      On 3/10/26 6:56 AM, Jonathan Wakely wrote:
>> >      >>>       > On Mon, 9 Mar 2026 at 15:18, Nathan Myers
>> >     <ncm@cantrip.org <mailto:ncm@cantrip.org>
>> >      >>>      <mailto:ncm@cantrip.org <mailto: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>
>> >      >>>      <mailto:ncm@cantrip.org <mailto:ncm@cantrip.org>>
>> >      >>>       >>> <mailto:ncm@cantrip.org <mailto:ncm@cantrip.org>
>> >     <mailto: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>
>> >      >>>      <https://gcc.gnu.org/PR106477 <https://gcc.gnu.org/
>> >     PR106477>> <https://
>> >      >>>       >>> gcc.gnu.org/PR106477 <http://gcc.gnu.org/PR106477>
>> >     <http://gcc.gnu.org/PR106477 <http://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.
>> >      >>>       >
>> >      >>>       > How would operator new return the real size to the caller?
>> >      >>>
>> >      >>>      It doesn't. std::allocator<>::_M_allocate_at_least() does
>> >     that job.
>> >      >>>
>> >      >>> What would _M_allocate_at_least do? As far as I know there is no
>> >      >>> interface in malloc that allows asking for and then using
>> >     returned bytes.
>> >      >>> malloc_usable_size (https://man7.org/linux/man-pages/man3/
>> >     <https://man7.org/linux/man-pages/man3/>
>> >      >>> malloc_usable_size.3.html <https://man7.org/linux/man-pages/
>> >     man3/ <https://man7.org/linux/man-pages/man3/>
>> >      >>> malloc_usable_size.3.html>)
>> >      >>> It is close but does not allow the number to be used, without
>> >     realloc
>> >      >>> call, according to docs.
>> >      >> _M_allocate_at_least may do anything it likes -- call jemalloc,
>> >      >> call mmap, whatever.
>> >      >
>> >      > But that means the memory comes from a different source, so can't be
>> >      > freed using the same allocator<T>::deallocate function.
>> >
>> >     ::operator delete and allocator<>::deallocate can rely on the
>> >     same ::__op_new_state to determine that the memory was allocated
>> >     by libstdc++ code, not user code, so it can do anything it needs
>> >     to.
>> >
>> > You do not need op_new_state at all, the check if operator was replaced
>> > can be implemented as (from Jonathan here: https://gcc.gnu.org/bugzilla/
>> > show_bug.cgi?id=106477 <https://gcc.gnu.org/bugzilla/show_bug.cgi?
>> > id=106477>)
>> >
>> > +  void* (*op_new)(std::size_t) = &::operator new;
>> > +  if (op_new == __gnu_cxx::__default_op_new)
>>
>> It doesn't matter much what method is used, provided it is fast
>> and robust.
>>
>> >     Whichever interface delivered the memory has certainly salted
>> >     away enough information to free it properly, because that is what
>> >     allocators do.
>> >
>> > Both memory allocated by allocate and allocate_at_least needs to
>> > be freed by allocator.deallocate. Assuming allocate will still use operator
>> > new and then malloc, then either:
>> > * you need to replace both (out of scope)
>> > * you need to have side channel information to decide where the memory
>> > should be deallocated.
>>
>> Yes, allocators and their deallocators need to communicate with one
>> another. Often that is achieved with bytes at negative offset from
>> the pointer returned.
>
> Yes, so if allocator::allocate uses malloc, and allocator::allocate_at_least uses other_malloc,
> that have different marking strategy (incompoatible with malloc), and requires memory to be released
> with other_free, the allocator<>::deallocate(ptr) needs to:
> * use free(ptr) if ptr was returned from allocator<>::allocate
> * other_free(ptr) if ptr was returned from allocator<>::allocate_at_least

Yes, so both allocate and allocate_at_least would need to reserve
extra memory in order to store a cookie before the return value, which
would say which function was used to allocate the memory. Then
deallocate would inspect the cookie to decide how to do the
deallocation. This adds overhead and uses extra memory, and can't be
done in the templated functions in the allocator class template,
because otherwise it becomes part of the ABI forever.

And the extra space and time overhead would affect all uses of
std::allocator, not only the ones that are opting in to the new API by
calling allocate_at_least.

And what would the new logic for allocate_at_least even do? Are we
going to reimplement an entire malloc, just to be able to return a few
more bytes? Using mmap doesn't give you a high performance allocator,
you still need to implement a malloc-like thing on top of raw mmap.
Using jemalloc isn't an option unless we bundle

I'm not persuaded that this is worth doing.

P0901 would have meant the additional logic happens in the right
place, in an operator new which is replaceable. I don't think doing it
in std::allocator is a good idea.


> The deallocator needs to match the allocation function, or it needs to be
> intercompatible. This is what is preventing _M_allocate_at_least from doing something
> different from malloc.
>>
>>
>> >      >> I really don't understand what is difficult about this. Is there
>> >      >> some prescribed constraint I am not aware of? I don't think the
>> >      >> Standard requires that we use C library facilities.
>> >      >>
>> >      >>>      All that is new in ::operator new is establishing for
>> >     std::allocator
>> >      >>>      that the user has not displaced ::op new(): seeing
>> >     __op_new_state==1,
>> >      >>>      allocate_at_least() knows it can do whatever it likes
>> >     because calls
>> >      >>>      to ::op new are not user-visible, so users cannot
>> >     determine whether
>> >      >>>      memory has been obtained from it.
>> >      >>>
>> >      >>>       > operator new can't encode the result in the return
>> >     value somehow,
>> >      >>>       > because that would be an ABI break for all callers that
>> >     aren't aware
>> >      >>>       > that operator new would be doing that now.
>> >      >>>       >
>> >      >>>       > Poking something to the stack seems a lot less portable
>> >     and more
>> >      >>>       > fragile than using a thread-local variable. And likely
>> >     to cause
>> >      >>>       > problems with -fstack-protector and/or AddressSanitizer.
>> >      >>>       > Maybe something with __builtin_return_address would be
>> >     possible, but
>> >      >>>       > operator new has no idea if the caller is opting in to
>> >     having its
>> >      >>>       > stack fiddled with (maybe the atomic was set by a
>> >     caller in another
>> >      >>>       > thread, and the caller in the current thread is not
>> >      >>>       > allocate_at_least).
>> >      >>>
>> >      >>>      Thread-local variables are a snake pit. Fortunately, we don't
>> >      >>>      need anything like that. Literally all the machinery is right
>> >      >>>      there in the code presented above, except the
>> >     implementation of
>> >      >>>      _M_allocate_at_least(), which may call jemalloc or whatever it
>> >      >>>      likes, and communicate its extra information to the caller via
>> >      >>>      an ordinary returned allocation_result<T*>{p, n}.
>> >      >>>
>> >      >>>      -N
>> >      >>>
>> >      >>
>> >      >
>> >
>>


More information about the Libstdc++ mailing list