On Thu, 10 Sept 2026 at 11:43, David Geier <[email protected]> wrote:
>
> To further reduce relcache memory usage we can additionally shrink the
> size consumed by struct RelationData:
[...]
> All RelationData objects are allocated in CacheMemoryContext. The size
> of CacheMemoryContext can be inspected via pg_backend_memory_contexts:
>
> branch  | used_bytes
> --------|---------------------------------
> master  | 617,984,696 bytes = ~589.36 MiB
> patched | 562,507,232 bytes = ~536.45 MiB
>
> We save ~53 MiB or ~10% of CacheMemoryContext per backend!

That's quite nice!

> The attached patch set passes tests and consists of the following
> individual patches:

[0001-0003]
I Haven't looked at these in detail. Yes, they provide the largest
savings, but that also makes them most complicated to review in
detail.  I'll stave that off for now.

> - 0004: Removes the rd_lockinfo member. RelationGetLockRelId() now
> computes it when needed. This also removes RelationInitLockInfo() and
> the initialization work associated with it.
> => sizeof(RelationData) == 304 bytes

Can we really rely on rd_rel always being valid when we need the
LockInfo? I'm OK with avoiding duplicating
rd_id/rd_lockinfo.lockRelId.relId, but I'm not sure dbId can always be
derived with rd_rel->relisshared whenever we need it.

> - 0005: Replaces the embedded partition key, descriptors, partition
> qual, validity flag, and memory contexts with one lazily allocated
> RelationPartitionInfo pointer.
> => sizeof(RelationData) == 264 bytes

I haven't worked on partitioning, so I'm not fully confident that this
has sufficiently low additional overhead to be worth applying.

> - 0006: Removes rd_fkeyvalid by using RELCACHE_FKEYLIST_NOT_LOADED as
> the initial state of rd_fkeylist. NIL continues to mean that the list
> was computed and no foreign keys were found. This patch is not strictly
> needed because it currently doesn't further reduce the size. The same we
> could with RelationPartitionInfo::partcheckvalid.
> => sizeof(RelationData) == 264 bytes

I'm not a fan of this change.  Sentinel values *can* have their place,
but I really don't like non-NULL values being used to signal "invalid"
states, and seeing that it doesn't actually save any bytes I'd prefer
to not add this complication.

> - 0007: Removes rd_index member that always pointed inside
> rd_indextuple. Callers now use RelationGetIndex(), which applies
> GETSTRUCT() to that tuple.
> => sizeof(RelationData) == 256 bytes

This seems fine to me.

> - 0008: Adds a static assertion that sizeof(RelationData) <= 256.
> => sizeof(RelationData) == 256 bytes

I'm not a fan of this, because we generally don't add assertions on
struct sizes unless it's critically important a struct remains the
asserted size (or, in this case, doesn't become larger than that).
In this case, I don't think it's critically important that the struct
fits in the 256-byte aset bucket, given that relcache memory usage has
never been capped, and a slab context would similarly do the trick for
avoiding memory usage cliffs when the size of the struct is increased.

> Note that the necessity to arrive at 256 bytes stems from ASET's
> allocation granularity being powers of two. To profit from further size
> reductions of RelationData, we would need to use a SLAB memory context
> because it's unlikely that we'll get to 128 bytes.

Let's start with palloc-ing all RelationData into Slab contexts (like
attached, tagged with .nocfbot to avoid your patch's CI).  This allows
us to start saving bytes immediately, and immediately improves memory
usage for every reduction in size so that the merits of each later
patch can be evaluated separately.  This will be very useful if we
can't agree on some of the changes that are necessary to reduce the
size down to the next smaller aset size bucket.


Kind regards,

Matthias van de Meent
Databricks (https://www.databricks.com)
From dc4466ddddcf87edd586da641a2d88a46cef7fc6 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <[email protected]>
Date: Mon, 14 Sep 2026 14:02:26 +0200
Subject: [PATCH v1] relcache: Use Slab context for RelationData allocs

This avoids the alignment wastage that AllocSet contexts have, saving
lots when a backend has lots of relations in its relcache.
---
 src/backend/utils/cache/relcache.c | 26 +++++++++++++++++++++++---
 1 file changed, 23 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index f475d703977..7deaf041f94 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -137,6 +137,14 @@ typedef struct relidcacheent
 
 static HTAB *RelationIdCache;
 
+/*
+ * Slab context for all RelationData allocations.
+ *
+ * In large catalogs, aset.c's chunk sizing rules would waste significant
+ * amounts of memory, so a Slab context is used instead.
+ */
+static MemoryContext	RelCacheMemoryContext;
+
 /*
  * This flag is false until we have prepared the critical relcache entries
  * that are needed to do indexscans on the tables read by relcache building.
@@ -421,13 +429,15 @@ AllocateRelationDesc(Form_pg_class relp)
 	Form_pg_class relationForm;
 
 	/* Relcache entries must live in CacheMemoryContext */
-	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+	oldcxt = MemoryContextSwitchTo(RelCacheMemoryContext);
 
 	/*
 	 * allocate and zero space for new relation descriptor
 	 */
 	relation = palloc0_object(RelationData);
 
+	MemoryContextSwitchTo(CacheMemoryContext);
+
 	/* make sure relation is marked as having no open file yet */
 	relation->rd_smgr = NULL;
 
@@ -1892,11 +1902,14 @@ formrdesc(const char *relationName, Oid relationReltype,
 	int			i;
 	bool		has_not_null;
 
+	MemoryContextSwitchTo(RelCacheMemoryContext);
 	/*
 	 * allocate new relation desc, clear all fields of reldesc
 	 */
 	relation = palloc0_object(RelationData);
 
+	MemoryContextSwitchTo(CacheMemoryContext);
+
 	/* make sure relation is marked as having no open file yet */
 	relation->rd_smgr = NULL;
 
@@ -3575,13 +3588,15 @@ RelationBuildLocalRelation(const char *relname,
 	if (!CacheMemoryContext)
 		CreateCacheMemoryContext();
 
-	oldcxt = MemoryContextSwitchTo(CacheMemoryContext);
+	oldcxt = MemoryContextSwitchTo(RelCacheMemoryContext);
 
 	/*
 	 * allocate a new relation descriptor and fill in basic state fields.
 	 */
 	rel = palloc0_object(RelationData);
 
+	MemoryContextSwitchTo(CacheMemoryContext);
+
 	/* make sure relation is marked as having no open file yet */
 	rel->rd_smgr = NULL;
 
@@ -4021,6 +4036,10 @@ RelationCacheInitialize(void)
 	RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,
 								  &ctl, HASH_ELEM | HASH_BLOBS);
 
+	RelCacheMemoryContext =
+		SlabContextCreate(CacheMemoryContext, "Relation bump cxt",
+						  8192, sizeof(RelationData));
+
 	/*
 	 * reserve enough in_progress_list slots for many cases
 	 */
@@ -6264,7 +6283,8 @@ load_relcache_init_file(bool shared)
 			rels = repalloc_array(rels, Relation, max_rels);
 		}
 
-		rel = rels[num_rels++] = (Relation) palloc(len);
+		rel = rels[num_rels++] =
+			(Relation) MemoryContextAlloc(RelCacheMemoryContext, len);
 
 		/* then, read the Relation structure */
 		if (fread(rel, 1, len, fp) != len)
-- 
2.54.0 (Apple Git-157)

Reply via email to