From 3686e660446facfb2d64683286176887914cd9fd Mon Sep 17 00:00:00 2001
From: Ronan Dunklau <ronan.dunklau@aiven.io>
Date: Tue, 20 Jun 2023 16:17:32 +0200
Subject: [PATCH v1] Add options to tune malloc.

Add a new GUC glibc_malloc_max_trim_threshold which is used only when
being compiled against glibc.

This GUC adjusts the glibc's malloc options M_MMAP_THRESHOLD and M_TRIM_THRESHOLD.
The M_MMAP_THRESHOLD will be set to half M_TRIM_THRESHOLD. We cap the
value to the current work_mem, as it doesn't make much sense to reserve
more memory than that.

This new GUC allows the user to control how aggresively malloc will actually
return memory from the heap to the kernel, and when it should start
using mmap for bigger allocations. Depending on the workload, a user can
reduce the residual footprint of a long session (by reducing the trim
threshold to a fairly low value) or on the contrary make sure that
malloc keeps a sizable free chunk at the end of the heap for future
allocations.

On some benchmarks heavily dependent on memory allocations, like sorts,
this can dramatically influence performance.
---
 src/backend/utils/init/postinit.c             |   6 +
 src/backend/utils/misc/guc_tables.c           |  16 ++-
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/backend/utils/mmgr/Makefile               |   1 +
 src/backend/utils/mmgr/malloc_tuning.c        | 122 ++++++++++++++++++
 src/backend/utils/mmgr/meson.build            |   1 +
 src/include/utils/guc_hooks.h                 |   2 +
 src/include/utils/memutils.h                  |  10 ++
 8 files changed, 158 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/utils/mmgr/malloc_tuning.c

diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 561bd13ed2..707f20606d 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -808,6 +808,12 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	InitCatalogCache();
 	InitPlanCache();
 
+	/* Adjust malloc options if needed.
+	 * This is done here because the implementation can vary depending on the
+	 * type of backend.
+	 */
+	MallocAdjustSettings();
+
 	/* Initialize portal manager */
 	EnablePortalManager();
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 71e27f8eb0..f1e03dc306 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2338,7 +2338,7 @@ struct config_int ConfigureNamesInt[] =
 		},
 		&work_mem,
 		4096, 64, MAX_KILOBYTES,
-		NULL, NULL, NULL
+		NULL, &assign_work_mem, NULL
 	},
 
 	{
@@ -2364,6 +2364,20 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"glibc_malloc_max_trim_threshold", PGC_USERSET, RESOURCES_MEM,
+			gettext_noop("Sets the maximum value for glibc's M_TRIM_THRESHOLD option."),
+			gettext_noop("This controls how much memory glibc's will not return to the "
+					"OS once freed. An idle backend can thus consume that much memory "
+					"even if not in used. The default (-1) value disable static tuning "
+					"and relies on the default dynamic adjustment"),
+			GUC_UNIT_KB
+		},
+		&glibc_malloc_max_trim_threshold,
+		-1, -1, MAX_KILOBYTES,
+		NULL, &assign_glibc_trim_threshold, NULL
+	},
+
 	/*
 	 * We use the hopefully-safely-small value of 100kB as the compiled-in
 	 * default for max_stack_depth.  InitializeGUCOptions will increase it if
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e4c0269fa3..dc70a1dfa3 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -157,6 +157,7 @@
 					#   windows
 					#   mmap
 					# (change requires restart)
+#glibc_malloc_max_trim_threshold = -1 # Only used with glibc's malloc. -1 disable it
 #min_dynamic_shared_memory = 0MB	# (change requires restart)
 #vacuum_buffer_usage_limit = 256kB	# size of vacuum and analyze buffer access strategy ring;
 					# 0 to disable vacuum buffer access strategy;
diff --git a/src/backend/utils/mmgr/Makefile b/src/backend/utils/mmgr/Makefile
index dae3432c98..88c5f7e23a 100644
--- a/src/backend/utils/mmgr/Makefile
+++ b/src/backend/utils/mmgr/Makefile
@@ -18,6 +18,7 @@ OBJS = \
 	dsa.o \
 	freepage.o \
 	generation.o \
+	malloc_tuning.o \
 	mcxt.o \
 	memdebug.o \
 	portalmem.o \
diff --git a/src/backend/utils/mmgr/malloc_tuning.c b/src/backend/utils/mmgr/malloc_tuning.c
new file mode 100644
index 0000000000..935420e74f
--- /dev/null
+++ b/src/backend/utils/mmgr/malloc_tuning.c
@@ -0,0 +1,122 @@
+#include "postgres.h"
+
+#include "utils/guc_hooks.h"
+#include "utils/memutils.h"
+#include "miscadmin.h"
+
+
+/*
+ * Implementation speficic GUCs. Those are defined even if we use another implementation, but will have
+ * no effect in that case.
+ */
+
+int glibc_malloc_max_trim_threshold;
+
+/*
+ * Depending on the malloc implementation used, we may want to
+ * tune it.
+ * In this first version, the only tunable library is glibc's malloc
+ * implementation.
+ */
+/* GLIBC implementation */
+
+void
+assign_work_mem(int newval, void* extra)
+{
+	work_mem = newval;
+	MallocAdjustSettings();
+}
+
+void
+assign_glibc_trim_threshold(int newval, void* extra)
+{
+	glibc_malloc_max_trim_threshold = newval;
+	MallocAdjustSettings();
+}
+
+#if defined(__GLIBC__)
+#include <malloc.h>
+#include <limits.h>
+#include <bits/wordsize.h>
+
+int previous_mmap_threshold = -1;
+int previous_trim_threshold = -1;
+
+/* For GLIBC's malloc, we want to avoid having too many mmap'd memory regions,
+ * and also to avoid repeatingly allocating / releasing memory from the system.
+ *
+ * The default configuration of malloc adapt's its M_MMAP_THRESHOLD and M_TRIM_THRESHOLD
+ * dynamically when previoulsy mmaped blocks are freed.
+ *
+ * This isn't really sufficient for our use case, as we may end up with a trim
+ * threshold which repeatedly releases work_mem memory to the system.
+ *
+ * Instead of letting malloc dynamically tune itself, for values up to 32MB we
+ * ensure that work_mem will fit both bellow M_MMAP_THRESHOLD and
+ * M_TRIM_THRESHOLD. The rationale for this is that once a backend has allocated
+ * this much memory, it is likely to use it again.
+ *
+ * To keep up with malloc's default behaviour, we set M_TRIM_THRESHOLD to
+ * M_MMAP_THRESHOLD * 2 so that work_mem blocks can avoid being released too
+ * early.
+ *
+ * Newer versions of malloc got rid of the MALLOC_MMAP_THRESHOLD upper limit,
+ * but we still enforce the values it sets to avoid wasting too much memory if we have a huge
+ * work_mem which is used only once.
+ */
+
+# if __WORDSIZE == 32
+#  define MMAP_THRESHOLD_MAX (512 * 1024)
+# else
+#  define MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
+# endif
+
+void
+MallocAdjustSettings()
+{
+	int uncapped_mmap_threshold,
+		mmap_threshold,
+		trim_threshold;
+	long base_target;
+	/* If static malloc tuning is disabled, bail out. */
+	if (glibc_malloc_max_trim_threshold == -1)
+		return;
+	/* We don't want to adjust anything in the postmaster process, as that would
+	 * disable dynamic adjustment for any child process*/
+	if ((MyProcPid == PostmasterPid) ||
+		((MyBackendType != B_BACKEND) &&
+		 (MyBackendType != B_BG_WORKER)))
+		return;
+	base_target = Min((long) work_mem * 1024, (long) glibc_malloc_max_trim_threshold / 2 * 1024);
+	/* To account for overhead, add one more memory page to that. */
+	base_target += 4096;
+	uncapped_mmap_threshold = Min(INT_MAX, base_target);
+	/* Cap mmap_threshold to MMAP_THRESHOLD_MAX */
+	mmap_threshold = Min(MMAP_THRESHOLD_MAX, uncapped_mmap_threshold);
+	/* Set trim treshold to two times the uncapped value */
+	trim_threshold = Min(INT_MAX, (long) uncapped_mmap_threshold * 2);
+	if (mmap_threshold != previous_mmap_threshold)
+	{
+		mallopt(M_MMAP_THRESHOLD, mmap_threshold);
+		previous_mmap_threshold = mmap_threshold;
+	}
+
+	if (trim_threshold != previous_trim_threshold)
+	{
+		mallopt(M_TRIM_THRESHOLD, trim_threshold);
+		/* If we reduce the trim_threshold, ask malloc to actually trim it.
+		 * This allows us to recover from a bigger work_mem set up once, then
+		 * reset back to a smaller value.
+		 */
+		if (trim_threshold < previous_trim_threshold)
+			malloc_trim(trim_threshold);
+		previous_trim_threshold = trim_threshold;
+	}
+}
+
+/* Default no-op implementation for others malloc providers. */
+#else
+void MallocAdjustSettings()
+{
+}
+#endif
diff --git a/src/backend/utils/mmgr/meson.build b/src/backend/utils/mmgr/meson.build
index e6ebe145ea..53c1c85524 100644
--- a/src/backend/utils/mmgr/meson.build
+++ b/src/backend/utils/mmgr/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
   'dsa.c',
   'freepage.c',
   'generation.c',
+  'malloc_tuning.c',
   'mcxt.c',
   'memdebug.c',
   'portalmem.c',
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 2ecb9fc086..e3644d4323 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -59,6 +59,7 @@ extern bool check_default_with_oids(bool *newval, void **extra,
 									GucSource source);
 extern bool check_effective_io_concurrency(int *newval, void **extra,
 										   GucSource source);
+extern void assign_glibc_trim_threshold(int newval, void* extra);
 extern bool check_huge_page_size(int *newval, void **extra, GucSource source);
 extern const char *show_in_hot_standby(void);
 extern bool check_locale_messages(char **newval, void **extra, GucSource source);
@@ -157,6 +158,7 @@ extern bool check_wal_consistency_checking(char **newval, void **extra,
 										   GucSource source);
 extern void assign_wal_consistency_checking(const char *newval, void *extra);
 extern void assign_xlog_sync_method(int new_sync_method, void *extra);
+extern void assign_work_mem(int newval, void *extra);
 extern bool check_io_direct(char **newval, void **extra, GucSource source);
 extern void assign_io_direct(const char *newval, void *extra);
 
diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h
index 21640d62a6..b502c1b0ff 100644
--- a/src/include/utils/memutils.h
+++ b/src/include/utils/memutils.h
@@ -93,6 +93,16 @@ extern void MemoryContextStatsDetail(MemoryContext context, int max_children,
 extern void MemoryContextAllowInCriticalSection(MemoryContext context,
 												bool allow);
 
+/* Interface to tune underlying malloc implementation in mcxt.c.
+ * The implementation can only rely on GUCs for now, but it could be profitable
+ * to collect statistics about individual palloc / pfree cycle to determine the
+ * optimum size of certain values.
+ */
+extern void MallocAdjustSettings(void);
+
+/* Malloc-implementation specific GUCs */
+extern PGDLLIMPORT int glibc_malloc_max_trim_threshold;
+
 #ifdef MEMORY_CONTEXT_CHECKING
 extern void MemoryContextCheck(MemoryContext context);
 #endif
-- 
2.41.0

