[glibc/fw/malloc-batch-unsorted-merge-llm-16] Surface-level coalescing in __libc_free_batched_loop

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

commit 8727aa7d8ee046faefad989ec12f91140a7f3daa
Author: Florian Weimer <[email protected]>
Date:   Sun Jun 28 12:37:18 2026 +0200

    Surface-level coalescing in __libc_free_batched_loop
    
    Assisted-by: LLM

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

diff --git a/malloc/malloc.c b/malloc/malloc.c
index 99ba94454e..3b92cb6e04 100644
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -3007,41 +3007,87 @@ tcache_free_init (void *mem)
   __libc_free (mem);
 }
 
-/* If the arena does not change between chunks, keep the lock.  */
-static inline void
-__libc_free_batched_loop (bool do_lock, mstate av, mchunkptr p, INTERNAL_SIZE_T size,
+/* Adjacent tcache entries are merged locally before touching the heap,
+   so that _int_free_merge_chunk is called once for the whole block
+   instead of inserting and immediately unlinking individual chunks.
+   The arena lock acquisition is delayed until the first flush and
+   kept between flushes to the same arena.  */
+static __always_inline void
+__libc_free_batched_loop (bool do_lock, mchunkptr p, INTERNAL_SIZE_T size,
 			  tcache_perthread_struct *tc, size_t tc_idx)
 {
   /* Empty half of the tcache, for a hysteresis effect.  */
   unsigned int to_free = mp_.tcache_count / 2;
+  INTERNAL_SIZE_T chunk_size = size;
 
+  /* The currently locked arena.  */
+  mstate av;
   if (do_lock)
-    __libc_lock_lock (av->mutex);
-
-  _int_free_merge_chunk (av, p, size);
+    /* Lazily acquire the lock before the first _int_free_merge_chunk call.  */
+    av = NULL;
+  else
+    /* No locking.  Chunks are always in the main arena.  */
+    av = &main_arena;
 
+  /* Accumulate adjacent chunks from the tcache into [p, p+size)
+     without modifying the heap.  Flush to the heap when a
+     non-adjacent chunk is encountered.  */
   while (tc->entries[tc_idx] != NULL && to_free > 0)
     {
       void *mem = tcache_get_n (tc, tc_idx, &tc->entries[tc_idx], false);
-      p = mem2chunk (mem);
-      size = chunksize (p);
+      mchunkptr q = mem2chunk (mem);
 
-      /* Lock a different arena if necessary.  */
-      if (do_lock)
+      if ((char *) q + chunk_size == (char *) p)
+	{
+	  /* q is immediately before our block, extend backward.  */
+	  p = q;
+	  size += chunk_size;
+	}
+      else if ((char *) p + size == (char *) q)
+	{
+	  /* q is immediately after our block, extend forward.  */
+	  size += chunk_size;
+	}
+      else
 	{
-	  mstate chunk_av = arena_for_chunk (p);
-	  if (chunk_av != av)
+	  /* Not adjacent.  Flush the accumulated block.  */
+	  if (do_lock)
 	    {
-	      __libc_lock_unlock (av->mutex);
-	      av = chunk_av;
-	      __libc_lock_lock (av->mutex);
+	      mstate new_av = arena_for_chunk (p);
+	      if (new_av != av)
+		{
+		  if (av != NULL)
+		    __libc_lock_unlock (av->mutex);
+		  av = new_av;
+		  __libc_lock_lock (av->mutex);
+		}
 	    }
+#ifdef MALLOC_DEBUG
+	  set_head (p, size | (chunksize_nomask (p) & SIZE_BITS));
+#endif
+	  _int_free_merge_chunk (av, p, size);
+	  p = q;
+	  size = chunk_size;
 	}
-
-      _int_free_merge_chunk (av, p, size);
       to_free--;
     }
 
+  /* Flush the remaining accumulated block.  */
+  if (do_lock)
+    {
+      mstate new_av = arena_for_chunk (p);
+      if (new_av != av)
+	{
+	  if (av != NULL)
+	    __libc_lock_unlock (av->mutex);
+	  av = new_av;
+	  __libc_lock_lock (av->mutex);
+	}
+    }
+#ifdef MALLOC_DEBUG
+  set_head (p, size | (chunksize_nomask (p) & SIZE_BITS));
+#endif
+  _int_free_merge_chunk (av, p, size);
   if (do_lock)
     __libc_lock_unlock (av->mutex);
 }
@@ -3058,9 +3104,9 @@ __libc_free_batched (mchunkptr p, INTERNAL_SIZE_T size,
     return malloc_printerr_tail ("free(): invalid size (batch)");
 
   if (SINGLE_THREAD_P)
-    __libc_free_batched_loop (false, &main_arena, p, size, tc, tc_idx);
+    __libc_free_batched_loop (false, p, size, tc, tc_idx);
   else
-    __libc_free_batched_loop (true, arena_for_chunk (p), p, size, tc, tc_idx);
+    __libc_free_batched_loop (true, p, size, tc, tc_idx);
 }
 
 void