[PATCH] benchtest: malloc tcache hotpath benchtest.

Cupertino Miranda cupertino.miranda@oracle.com
Thu Apr 24 18:05:58 GMT 2025


Hi everyone,

This patch is introduces an adaptation of bench-malloc-thread, however forcing all
measured allocs and frees to only use tcache, making it a preferable
benchmark to evaluate performance improvements or degradation from
tcache hotpath changes.
Experiments so far show much less fluctuation of similar consecutive
executions.

Looking forward to your comments.

Best regards,
Cupertino

Existing benchtests for malloc infrastructure seem to be rather generic
to test global malloc implementation performance.  This new benchtest
focus on reducing any non tcache related side effects, allowing to more
realistically predict performance impacts of tcache code changes.
The test was inpired in bench-[cm]alloc-thread code, with the following
changes:
 - forces single thread execution, reducing concurrency side-effects,
   like cache incoherence penalties due to simultaneous parallel writes
   to same cache pages;
 - it prefills and saturates all tcache bins with chunks, before
   starting to collect any measurements;
 - input argument is now a max_allocation multipler, allowing it to
   increase max_allocation.

For the purpose of measuring current overhead on tcache hotpaths, only
the execution with argument as 1 is relevant, since current tcaches are
limitted to allocations up to 1kb.

---
 benchtests/Makefile              |   7 +
 benchtests/bench-malloc-tcache.c | 268 +++++++++++++++++++++++++++++++
 2 files changed, 275 insertions(+)
 create mode 100644 benchtests/bench-malloc-tcache.c

diff --git a/benchtests/Makefile b/benchtests/Makefile
index cccee62eb0..5275bed457 100644
--- a/benchtests/Makefile
+++ b/benchtests/Makefile
@@ -331,6 +331,7 @@ bench-malloc := \
   calloc-thread \
   malloc-simple \
   malloc-thread \
+  malloc-tcache \
   # bench-malloc
 else
 bench-malloc := $(filter malloc-%,${BENCHSET})
@@ -456,6 +457,7 @@ VALIDBENCHSETNAMES := \
   hash-benchset \
   malloc-simple \
   malloc-thread \
+  malloc-tcache \
   math-benchset \
   stdio-benchset \
   stdio-common-benchset \
@@ -498,6 +500,11 @@ bench-malloc: $(binaries-bench-malloc)
 			echo "Running $${run} $${thr}"; \
 			$(run-bench) $${thr} > $${run}-$${thr}.out; \
 		done;\
+	  elif basename $${run} | grep -q "bench-[cm]alloc-tcache"; then \
+		for thr in 1 8 32 128 512 2048 4096; do \
+			echo "Running $${run} $${thr}"; \
+			$(run-bench) $${thr} > $${run}-$${thr}.out; \
+		done;\
 	  else \
 		for thr in 8 16 32 64 128 256 512 1024 2048 4096; do \
 		  echo "Running $${run} $${thr}"; \
diff --git a/benchtests/bench-malloc-tcache.c b/benchtests/bench-malloc-tcache.c
new file mode 100644
index 0000000000..c1dbd81b3d
--- /dev/null
+++ b/benchtests/bench-malloc-tcache.c
@@ -0,0 +1,268 @@
+/* Benchmark tcache hotpath allocations.
+   Copyright (C) 2013-2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#ifndef TEST_FUNC
+# define TEST_FUNC(size) malloc(size)
+# define TEST_NAME "malloc"
+#endif
+
+#include <errno.h>
+#include <math.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <unistd.h>
+
+#include "bench-timing.h"
+#include "json-lib.h"
+
+/* Benchmark duration in seconds.  */
+#define BENCHMARK_DURATION	10
+#define RAND_SEED		88
+
+/* Maximum memory that can be allocated at any one time is:
+
+   WORKING_SET_SIZE * MAX_ALLOCATION_SIZE * alloc_multiplier
+
+   However due to the distribution of the random block sizes
+   the typical amount allocated will be much smaller.  */
+#define WORKING_SET_SIZE	1024
+
+#define MIN_ALLOCATION_SIZE	4
+#define MAX_ALLOCATION_SIZE	1024
+
+#define TCACHE_FILL_COUNT	7
+#define TCACHE_SIZE_OFFSET	16
+
+size_t alloc_multiplier;
+
+/* Get a random block size with an inverse square distribution.  */
+static unsigned int
+get_block_size (unsigned int rand_data)
+{
+  /* Inverse square.  */
+  const float exponent = -2;
+  /* Minimum value of distribution.  */
+  const float dist_min = MIN_ALLOCATION_SIZE;
+  /* Maximum value of distribution.  */
+  const float dist_max = MAX_ALLOCATION_SIZE * alloc_multiplier;
+
+  float min_pow = powf (dist_min, exponent + 1);
+  float max_pow = powf (dist_max, exponent + 1);
+
+  float r = (float) rand_data / RAND_MAX;
+
+  return (unsigned int) powf ((max_pow - min_pow) * r + min_pow,
+			      1 / (exponent + 1));
+}
+
+#define NUM_BLOCK_SIZES	8000
+#define NUM_OFFSETS	((WORKING_SET_SIZE) * 4)
+
+static unsigned int random_block_sizes[NUM_BLOCK_SIZES];
+static unsigned int random_offsets[NUM_OFFSETS];
+
+static void
+init_random_values (void)
+{
+  for (size_t i = 0; i < NUM_BLOCK_SIZES; i++)
+    random_block_sizes[i] = get_block_size (rand ());
+
+  for (size_t i = 0; i < NUM_OFFSETS; i++)
+    random_offsets[i] = rand () % WORKING_SET_SIZE;
+}
+
+static unsigned int
+get_random_block_size (unsigned int *state)
+{
+  unsigned int idx = *state;
+
+  if (idx >= NUM_BLOCK_SIZES - 1)
+    idx = 0;
+  else
+    idx++;
+
+  *state = idx;
+
+  return random_block_sizes[idx];
+}
+
+static unsigned int
+get_random_offset (unsigned int *state)
+{
+  unsigned int idx = *state;
+
+  if (idx >= NUM_OFFSETS - 1)
+    idx = 0;
+  else
+    idx++;
+
+  *state = idx;
+
+  return random_offsets[idx];
+}
+
+static volatile bool timeout;
+
+static void
+alarm_handler (int signum)
+{
+  timeout = true;
+}
+
+/* Allocate and free blocks in a random order.  */
+static size_t
+malloc_benchmark_loop (void **ptr_arr)
+{
+  unsigned int offset_state = 0, block_state = 0;
+  size_t iters = 0;
+
+  while (!timeout)
+    {
+      unsigned int next_idx = get_random_offset (&offset_state);
+      unsigned int next_block = get_random_block_size (&block_state);
+
+      free (ptr_arr[next_idx]);
+
+      ptr_arr[next_idx] = TEST_FUNC (next_block);
+
+      iters++;
+    }
+
+  return iters;
+}
+
+static void
+fill_tcache(void)
+{
+  void *ptrs[TCACHE_FILL_COUNT];
+  for (int i = MIN_ALLOCATION_SIZE;
+       i <= MAX_ALLOCATION_SIZE;
+       i += TCACHE_SIZE_OFFSET)
+    {
+      for (int j = 0; j < TCACHE_FILL_COUNT; j++)
+	ptrs[j] = TEST_FUNC (i);
+
+      for (int j = 0; j < TCACHE_FILL_COUNT; j++)
+	free (ptrs[j]);
+    }
+}
+
+static timing_t
+do_benchmark (size_t *iters)
+{
+  timing_t elapsed = 0;
+
+  timing_t start, stop;
+  void *working_set[WORKING_SET_SIZE];
+
+  memset (working_set, 0, sizeof (working_set));
+
+  /* Fill tcache bins with freed chunks. */
+  fill_tcache ();
+
+  TIMING_NOW (start);
+  *iters = malloc_benchmark_loop (working_set);
+  TIMING_NOW (stop);
+
+  TIMING_DIFF (elapsed, start, stop);
+
+  return elapsed;
+}
+
+static void usage(const char *name)
+{
+  fprintf (stderr, "%s: <alloc_multiplier>\n", name);
+  exit (1);
+}
+int
+main (int argc, char **argv)
+{
+  timing_t cur;
+  size_t iters = 0;
+  json_ctx_t json_ctx;
+  double d_total_s, d_total_i;
+  struct sigaction act;
+
+  if (argc == 1)
+    alloc_multiplier = 1;
+  else if (argc == 2)
+    {
+      long ret;
+
+      errno = 0;
+      ret = strtol(argv[1], NULL, 10);
+
+      if (errno || ret == 0)
+	usage(argv[0]);
+
+      alloc_multiplier = ret;
+    }
+  else
+    usage(argv[0]);
+  init_random_values ();
+
+  json_init (&json_ctx, 0, stdout);
+
+  json_document_begin (&json_ctx);
+
+  json_attr_string (&json_ctx, "timing_type", TIMING_TYPE);
+
+  json_attr_object_begin (&json_ctx, "functions");
+
+  json_attr_object_begin (&json_ctx, TEST_NAME);
+
+  json_attr_object_begin (&json_ctx, "");
+
+  memset (&act, 0, sizeof (act));
+  act.sa_handler = &alarm_handler;
+
+  sigaction (SIGALRM, &act, NULL);
+
+  alarm (BENCHMARK_DURATION);
+
+  cur = do_benchmark (&iters);
+
+  struct rusage usage;
+  getrusage(RUSAGE_SELF, &usage);
+
+  d_total_s = cur;
+  d_total_i = iters;
+
+  json_attr_double (&json_ctx, "duration", d_total_s);
+  json_attr_double (&json_ctx, "iterations", d_total_i);
+  json_attr_double (&json_ctx, "time_per_iteration", d_total_s / d_total_i);
+  json_attr_double (&json_ctx, "max_rss", usage.ru_maxrss);
+
+  json_attr_double (&json_ctx, "min_size", MIN_ALLOCATION_SIZE);
+  json_attr_double (&json_ctx, "max_size", MAX_ALLOCATION_SIZE * alloc_multiplier);
+  json_attr_double (&json_ctx, "random_seed", RAND_SEED);
+
+  json_attr_object_end (&json_ctx);
+
+  json_attr_object_end (&json_ctx);
+
+  json_attr_object_end (&json_ctx);
+
+  json_document_end (&json_ctx);
+
+  return 0;
+}
-- 
2.30.2



More information about the Libc-alpha mailing list