[PATCH] malloc: Optimize the madvise behaviour on the main heap
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Thu Nov 20 17:21:27 GMT 2025
On 17/11/25 02:40, Dev Jain wrote:
>
> On 14/11/25 9:57 pm, Adhemerval Zanella Netto wrote:
>>
>> On 09/11/25 04:29, Dev Jain wrote:
>>> Linux handles virtual memory in Virtual Memory Areas (VMAs). The
>>> madvise(MADV_HUGEPAGE) call works on a VMA granularity, which sets the
>>> VM_HUGEPAGE flag on the VMA. Therefore, if we can guarantee that a VMA
>>> has been marked with VM_HUGEPAGE already, then we do not need to call
>>> madvise() on that VMA again.
>>>
>>> For mp_.thp_pagesize != 0, currently we align the new brk to the thp size.
>>> This means that after the first extension, all such brk extensions are
>>> guaranteed to produce an extension size >= thp size: madvise_thp() will
>>> invoke the madvise() syscall only if size >= thp size, and the other
>>> condition is related to the sysctl setting, wherein mp_.thp_mode will be
>>> same throughout the lifetime of the process. Therefore, currently we invoke
>>> the madvise() syscall on the heap on each extension, which is unnecessary.
>>>
>>> First, pass the total heap size, instead of the extension size, to
>>> madvise_thp: Linux does not care about the size passed, in case the
>>> madvise() syscall is invoked with MADV_HUGEPAGE flag, because the flag
>>> will be set on the entire VMA, no matter for what portion of the VMA
>>> the syscall is invoked.
>>>
>>> This enables us to do the following: if the old heap size >= thp size,
>>> we can guarantee that madvise() was invoked on one of the previous
>>> extensions of the heap. So, avoid making the syscall in this case.
>>>
>>> The tricky part is computing the size of the heap, i.e the current program
>>> break minus the initial program break. In case the first ever attempt at
>>> extending the break fails, mp_.sbrk_base will be set to an mmapped address.
>>> Therefore, we need some other way of remembering the initial location
>>> of the program break. We can reuse some code for this: MORECORE (0), when
>>> invoked for the first time ever, will give us the initial program break.
>>> ---
>>> The patch applies on 259adb087dd9. Built on Aarch64, all malloc tests pass.
>> I am not sure if the calculation of
>>
>>> malloc/malloc.c | 23 +++++++++++++++++++++--
>>> 1 file changed, 21 insertions(+), 2 deletions(-)
>>>
>>> diff --git a/malloc/malloc.c b/malloc/malloc.c
>>> index 0b21bdf1bd..277fb9e9ec 100644
>>> --- a/malloc/malloc.c
>>> +++ b/malloc/malloc.c
>>> @@ -1938,6 +1938,12 @@ struct malloc_par
>>> /* First address handed out by MORECORE/sbrk. */
>>> char *sbrk_base;
>>> + /* The initial location of program break. This will most likely be equal
>>> + to sbrk_base; in case the first ever extension attempt of brk fails,
>>> + sbrk_base will point to an mmapped address (see sysmalloc_mmap_fallback),
>>> + in which case these two values will not be equal. */
>>> + char *init_sbrk_base;
>>> +
>>> #if USE_TCACHE
>>> /* Maximum number of small buckets to use. */
>>> size_t tcache_small_bins;
>>> @@ -2667,6 +2673,9 @@ sysmalloc (INTERNAL_SIZE_T nb, mstate av)
>>> if (__glibc_unlikely (mp_.thp_pagesize != 0))
>>> {
>>> uintptr_t lastbrk = (uintptr_t) MORECORE (0);
>>> + if (mp_.init_sbrk_base == NULL)
>>> + mp_.init_sbrk_base = (char *) lastbrk;
>>> +
>>> uintptr_t top = ALIGN_UP (lastbrk + size, mp_.thp_pagesize);
>>> size = top - lastbrk;
>>> }
>>> @@ -2682,8 +2691,18 @@ sysmalloc (INTERNAL_SIZE_T nb, mstate av)
>>> if ((ssize_t) size > 0)
>>> {
>>> brk = (char *) (MORECORE ((long) size));
>>> - if (brk != (char *) (MORECORE_FAILURE))
>>> - madvise_thp (brk, size);
>>> + if (brk != (char *) (MORECORE_FAILURE)) {
>>> + size_t old_size = (size_t) (brk - mp_.init_sbrk_base);
>>> +
>>> + /*
>>> + If heap already marked with MADV_HUGEPAGE, skip madvise(). Note
>>> + that, we don't need to check mp_.init_sbrk_base != NULL; if it
>>> + is NULL, it implies that mp_.thp_pagesize == 0, in which case
>>> + madvise_thp() will not invoke madvise().
>>> + */
>>> + if (old_size < mp_.thp_pagesize)
>>> + madvise_thp (brk, old_size + size);
>> I am not sure if this calculation is fully correct, with a simple testcase:
>>
>> $ cat t.c
>> #include <stdlib.h>
>> #include <pthread.h>
>>
>> static void *tf (void* arg)
>> {
>> for (int i = 0; i < 1024; i++)
>> malloc (4096);
>> return NULL;
>> }
>>
>> int main ()
>> {
>> for (int i = 0; i < 1024; i++)
>> malloc (4096);
>>
>> pthread_t t;
>> pthread_create (&t, NULL, tf, NULL);
>> pthread_join (t, NULL);
>> }
>> $ strace -e madvise -f -E GLIBC_TUNABLES=glibc.malloc.hugetlb=1 elf/ld.so --library-path . ./t
>> madvise(0xaaaade400000, 3354624, MADV_HUGEPAGE) = -1 ENOMEM (Cannot allocate memory)
>> madvise(0xf7fa28ed0000, 65536, 0x66 /* MADV_??? */) = 0
>> strace: Process 313335 attached
>> [pid 313335] madvise(0xf7fa24000000, 2097152, MADV_HUGEPAGE) = 0
>> [pid 313335] madvise(0xf7fa28ed0000, 8314880, MADV_DONTNEED) = 0
>> [pid 313335] +++ exited with 0 +++
>> +++ exited with 0 +++
>>
>> Where without this patch:
>>
>> $ strace -e madvise -f -E GLIBC_TUNABLES=glibc.malloc.hugetlb=1 ./t
>> madvise(0xc3da42fb9000, 2097152, MADV_HUGEPAGE) = 0
>> madvise(0xc3da43200000, 2097152, MADV_HUGEPAGE) = 0
>> strace: Process 313347 attached
>> [pid 313347] madvise(0xfa19510a0000, 8314880, MADV_DONTNEED) = 0
>> [pid 313347] +++ exited with 0 +++
>> +++ exited with 0 +++
>
> Thanks for testing! Although without the patch, I get this:
>
> madvise(0xfffff8000000, 2097152, MADV_HUGEPAGE) = 0
> madvise(0xfffff8000000, 4194304, MADV_HUGEPAGE) = 0
> madvise(0xfffff8000000, 6291456, MADV_HUGEPAGE) = 0
> madvise(0xfffff7400000, 65536, 0x66 /* MADV_??? */) = -1 EINVAL (Invalid argument)
> strace: Process 3162240 attached
> [pid 3162240] madvise(0xfffff0000000, 2097152, MADV_HUGEPAGE) = 0
> [pid 3162240] madvise(0xfffff7400000, 8314880, MADV_DONTNEED) = 0
> [pid 3162240] +++ exited with 0 +++
> +++ exited with 0 +++
>
> I don't know where the EINVAL is coming from, that shouldn't be happening, because we
> have hardcoded MADV_HUGEPAGE everywhere. I played around and I think that when madvise() is not invoked
> ever, then strace shows it returning EINVAL. I am not familiar with how strace actually
> works and how to interpret the output.
The EINVAL most likely come from the size not being multiple of the large
page size, but I am not sure. I could not understand why the flags are being
shown as invalid (0x66), since I step on every madvise syscall trap and
I did not see this valug.
>
> But, there is an obvious error in this patch. I shouldn't be making the madvise() call
> on brk pointer, but on init_sbrk_base. Calling it on brk will cross the heap VMA, and
> the target range may include a hole. I checked the kernel source and it returns -ENOMEM
> if it encounters a gap.
>
> But I have a question. In madvise_thp, we align the pointer down if it is not aligned to
> pagesize. If before that pointer, there is a gap, then madvise() will return -ENOMEM,
> although, the madvise() operation will continue on the VMA which we have specified.
> So, the operation will do what we want it to do, but return -ENOMEM. Do you think
> this is something which needs to be fixed? The fix is simple: Align the pointer up
> instead of down, and subtract from size instead of add. Then we can check again
> whether size >= thp_pagesize, and continue with the syscall.
Are you referring to the case where the initial heap is not largepage aligned, then
the align down to a possible invalid address? If kernel returns ENOMEM in such case
(I have not checked yet), your suggestion might work although I think it will not
cover the initial page.
>
>>
>>
>> The second madvise call 'madvise(0xf7fa28ed0000, 65536, 0x66 /* MADV_??? */)'
>> is indeed even bogus. With gdb the value passed on the syscall are indeed:
>>
>> (gdb) catch syscall madvise
>> (gdb) r
>> [...]
>> (gdb) i r
>> x0 0xfffffffffffffff4 -12
>> x1 0x355000 3493888
>> x2 0xe 14
>> [...]
>>
>>> + }
>>> LIBC_PROBE (memory_sbrk_more, 2, brk, size);
>>> }
>>>
More information about the Libc-alpha
mailing list