[glibc/fw/malloc-batch-unsorted-merge-deep] malloc: Implement deep consolidation during merging

Florian Weimer via Glibc-cvs <[email protected]> Tue, 30 Jun 2026 17:31:37 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=1ff5c0de78681de6580cc199587298b94437ea83

commit 1ff5c0de78681de6580cc199587298b94437ea83
Author: Florian Weimer <[email protected]>
Date:   Tue Jun 30 13:55:16 2026 +0200

    malloc: Implement deep consolidation during merging
    
    Backwards consolidation happens unconditionally during batch
    processing.  Forward consolidation does not because of the
    need to skip over a chunk to get its metadata, and that is not
    possible if the chunk may be freed concurrently.

Diff:
---
 malloc/malloc.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 84 insertions(+), 9 deletions(-)

diff --git a/malloc/malloc.c b/malloc/malloc.c
index a4d70084c9..1c653a1495 100644
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -3008,6 +3008,68 @@ tcache_free_init (void *mem)
 }
 
 #if USE_TCACHE
+static __always_inline mstate
+__libc_free_batched_lock (bool do_lock, mstate av, mchunkptr p)
+{
+  if (do_lock && av == NULL)
+    {
+      av = arena_for_chunk (p);
+      __libc_lock_lock (av->mutex);
+    }
+  return av;
+}
+
+/* Try consolidating backwards.  *AV is locked opportunistically if
+   consolidation appears possible.  *EXPECTED_BEFORE is adjusted
+   accordingly.  */
+static __always_inline void
+__libc_free_batched_consolidate_backwards (bool do_lock, mstate *av,
+					   void **expected_before,
+					   mchunkptr p, INTERNAL_SIZE_T size)
+{
+  /* See the consolidation code in _int_free_merge_chunk.  */
+  if (!prev_inuse (p))
+    {
+      *av = __libc_free_batched_lock (do_lock, *av, p);
+      /* Recheck under the arena lock.  */
+      if (!prev_inuse (p))
+	{
+	  INTERNAL_SIZE_T prevsize = prev_size (p);
+	  p = chunk_at_offset(p, -((long) prevsize));
+	  if (__glibc_unlikely (chunksize(p) != prevsize))
+	    malloc_printerr ("corrupted size vs. prev_size"
+			     " while batch consolidating");
+	  unlink_chunk (*av, p);
+	  *expected_before = (void *) p - size;
+	}
+    }
+}
+
+/* Try consolidating forward.  *EXPECTED_AFTER is updated accordingly.  */
+static __always_inline void
+__libc_free_batched_consolidate_forward (bool do_lock, mstate av,
+					 void **expected_after,
+					 mchunkptr p, INTERNAL_SIZE_T size)
+{
+  if (do_lock && av == NULL)
+    /* It is not possible to check the in-use status of the next chunk
+       without the arena lock.  Its chunk size may change and move the
+       location of the in-use metadata bit.  */
+    return;
+  /* See the start of _int_free_create_chunk.  */
+  mchunkptr nextchunk = chunk_at_offset (p, size);
+  if (nextchunk == av->top)
+    return;
+  INTERNAL_SIZE_T nextsize = chunksize (nextchunk);
+  if (!inuse_bit_at_offset (nextchunk, nextsize))
+    {
+      unlink_chunk (av, nextchunk);
+      /* Set the in-use bit because _int_free_merge_chunk checks it.  */
+      set_inuse_bit_at_offset (nextchunk, nextsize);
+      *expected_after = (void *) nextchunk + nextsize;
+    }
+}
+
 /* Free the chunk at EXPECTED_BEFORE + SIZE.  Before that, if AV is
    NULL, obtain the arena from the chunk and lock it.  Otherwise,
    assume that AV matches the chunk.  Return the AV value.  */
@@ -3016,11 +3078,7 @@ __libc_free_batched_do_free (bool do_lock, mstate av, void *expected_before,
 			     void *expected_after, INTERNAL_SIZE_T size)
 {
   mchunkptr chunk = expected_before + size;
-  if (do_lock && av == NULL)
-    {
-      av = arena_for_chunk (chunk);
-      __libc_lock_lock (av->mutex);
-    }
+  av = __libc_free_batched_lock (do_lock, av, chunk);
   _int_free_merge_chunk (av, chunk, expected_after - (void *) chunk);
   return av;
 }
@@ -3051,8 +3109,11 @@ __libc_free_batched_loop (bool do_lock, mchunkptr p, INTERNAL_SIZE_T size,
      chunk that has not yet been committed to the lower-level
      allocator.  Start with the merge locations of p.  */
   void *expected_before = (void *) p - size;
+  __libc_free_batched_consolidate_backwards (do_lock, &av,
+					     &expected_before, p, size);
   void *expected_after = (void *) p + size;
-
+  __libc_free_batched_consolidate_forward (do_lock, av,
+					   &expected_after, p, size);
 
   while (tc->entries[tc_idx] != NULL && to_free > 0)
     {
@@ -3064,9 +3125,17 @@ __libc_free_batched_loop (bool do_lock, mchunkptr p, INTERNAL_SIZE_T size,
       /* Check if the chunk can be merged.  This does not require the
 	 lock because there is no structural change of the heap yet. */
       if (p == expected_before)
-	expected_before -= size;
+	{
+	  expected_before -= size;
+	  __libc_free_batched_consolidate_backwards (do_lock, &av,
+						     &expected_before, p, size);
+	}
       else if (p == expected_after)
-	expected_after += size;
+	{
+	  expected_after += size;
+	  __libc_free_batched_consolidate_forward (do_lock, av,
+						   &expected_after, p, size);
+	}
       else
 	{
 	  /* Deallocate the previous chunk that could not be merged.  */
@@ -3084,7 +3153,11 @@ __libc_free_batched_loop (bool do_lock, mchunkptr p, INTERNAL_SIZE_T size,
 
 	  /* Continue processing with the chunk that came from tcache.  */
 	  expected_before = (void *) p - size;
+	  __libc_free_batched_consolidate_backwards (do_lock, &av,
+						     &expected_before, p, size);
 	  expected_after = (void *) p + size;
+	  __libc_free_batched_consolidate_forward (do_lock, av,
+						   &expected_after, p, size);
 	}
 
       to_free--;
@@ -4108,7 +4181,7 @@ _int_free_merge_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size)
 
   free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);
 
-  /* Consolidate backward.  */
+  /* Consolidate backward.  See __libc_free_batched_consolidate_backwards.  */
   if (!prev_inuse(p))
     {
       INTERNAL_SIZE_T prevsize = prev_size (p);
@@ -4134,6 +4207,8 @@ static INTERNAL_SIZE_T
 _int_free_create_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size,
 			mchunkptr nextchunk, INTERNAL_SIZE_T nextsize)
 {
+  /* The start of this function (forward consolidation) is duplicated
+     in __libc_free_batched_consolidate_forward.  */
   if (nextchunk != av->top)
     {
       /* get and clear inuse bit */