Hi Team,

As a follow-up to the skip scan regression discussion, I tested a small
patch that introduces *static allocation/caching of `IndexAmRoutine` *objects
in `amapi.c`, removing the malloc/free overhead.

*Test setup :*
- Baseline: PG17 (commit before skip scan)
- After: PG18 build with skip scan (patched)
- pgbench scale=1, 100 partitions
- Query: `select count(*) from pgbench_accounts where bid = 0`
- Clients: 1, 4, 32
- Protocols: simple, prepared

*Results (tps, 10s runs) :*

Mode Clients Before (PG17) After (PG18 w/ static fix)

simple 1 23856 20332 (~15% lower)
simple 4 55299 53184 (~4% lower)
simple 32 79779 78347 (~2% lower)

prepared 1 26364 26615 (no regression)
prepared 4 55784 54437 (~2% lower)
prepared 32 84687 80374 (~5% lower)

This shows the static fix eliminates the severe ~50% regression previously
observed by Tomas, leaving only a small residual slowdown (*~2-15%*).

*Patch summary :*
- Cache `IndexAmRoutine` instances per AM OID instead of malloc/free per
call.
- Avoid `pfree(amroutine)` in hot paths.
- Keeps allocations stable across lookups, reducing malloc churn.

*Proposal :*
I suggest adopting this static allocation approach for PG18 to prevent
performance cliffs. Longer term, we can explore lighter-weight caching
mechanisms or further executor tuning.

*Patch attached for discussion.*

Thanks & Regards,
Athiyaman M

On Sat, Aug 30, 2025 at 4:37 AM Tomas Vondra <to...@vondra.me> wrote:

> On 8/29/25 21:03, Peter Geoghegan wrote:
> > On Fri, Aug 29, 2025 at 9:10 AM Tomas Vondra <to...@vondra.me> wrote:
> >> Peter, any thoughts on this. Do you think it's reasonable / feasible to
> >> push the fix?
> >
> > I don't feel comfortable pushing that fix today.
> >
>
> Understood.
>
> > Honestly, I'm still not sure what to do. My proposal was to just
> > remove the totally unused options support function, which is probably
> > fine. But since I don't really know why Alexander ever added the
> > "options" support function in the first place (I don't even see a
> > theoretical benefit), I'm not quite prepared to say that I know that
> > it's okay to remove it now.
> >
>
> Right. I think removing the "options" is the only feasible solution for
> PG18 at this point. Either that or nothing. The other patch is far too
> invasive.
>
> As for why the support procedure was added to existing index AMs, I
> don't know. I suppose it as mostly for consistency, so that custom
> oclasses could opclasses could use that. I have no idea if there are
> plausible custom opclasses using this.
>
> I'm not sure how I feel about removing the support proc. It feels pretty
> arbitrary and fragile, and IIRC it doesn't even address the perf issue
> (add a couple partitions and it'll hit the same issue). It just restores
> the "threshold" to where it was for PG17. And it's fragile, because we
> have no protections about hitting this glibc-specific behavior again. It
> takes one new flag added somewhere, and we'll not even notice it.
>
> So after thinking about this a bit more, and refreshing the context, I
> think the right solution for PG18 is to do nothing.
>
> regards
>
> --
> Tomas Vondra
>
>
>
>
From c80b2a07733112e23b70ed5b309f177f6bf79ef6 Mon Sep 17 00:00:00 2001
From: athiyaman-m <heroathi...@gmail.com>
Date: Wed, 10 Sep 2025 12:08:56 +0530
Subject: [PATCH] Use static allocation for IndexAmRoutine in amapi.c to reduce
 malloc/free overhead

This avoids repeated palloc/free cycles by keeping the routine
structure in static memory. Initial pgbench tests show that this
does not regress performance and may slightly improve efficiency
at higher client counts.

Signed-off-by: athiyaman-m <heroathi...@gmail.com>
---
 src/backend/access/index/amapi.c | 81 +++++++++++++++++++++++++++++---
 1 file changed, 75 insertions(+), 6 deletions(-)

diff --git a/src/backend/access/index/amapi.c b/src/backend/access/index/amapi.c
index d6b8dad4d5..d5481569d9 100644
--- a/src/backend/access/index/amapi.c
+++ b/src/backend/access/index/amapi.c
@@ -13,12 +13,19 @@
  */
 #include "postgres.h"
 
+#include <string.h>
+
 #include "access/amapi.h"
 #include "access/htup_details.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_opclass.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/hsearch.h"
+#include "utils/memutils.h"
+#include "utils/elog.h"
+#include "utils/pg_locale.h"
+#include "utils/builtins.h"
 
 
 /*
@@ -51,6 +58,10 @@ GetIndexAmRoutine(Oid amhandler)
  *
  * If the given OID isn't a valid index access method, returns NULL if
  * noerror is true, else throws error.
+ *
+ * This implementation caches a copy of the IndexAmRoutine per handler OID
+ * in TopMemoryContext so callers get a stable pointer and we avoid repeated
+ * per-call allocations/freeing of the routine struct.
  */
 IndexAmRoutine *
 GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
@@ -65,8 +76,7 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 	{
 		if (noerror)
 			return NULL;
-		elog(ERROR, "cache lookup failed for access method %u",
-			 amoid);
+		elog(ERROR, "cache lookup failed for access method %u", amoid);
 	}
 	amform = (Form_pg_am) GETSTRUCT(tuple);
 
@@ -102,8 +112,67 @@ GetIndexAmRoutineByAmId(Oid amoid, bool noerror)
 
 	ReleaseSysCache(tuple);
 
-	/* And finally, call the handler function to get the API struct. */
-	return GetIndexAmRoutine(amhandler);
+	/*
+	 * Cache management: keep a single HTAB (in TopMemoryContext) that maps
+	 * amhandler OIDs to a stored copy of IndexAmRoutine.
+	 *
+	 * Use HASH_BLOBS and store the copy of IndexAmRoutine inline in the entry.
+	 */
+	{
+		typedef struct IndexAmRoutineCacheEntry
+		{
+			IndexAmRoutine routine;	/* stored inline */
+		} IndexAmRoutineCacheEntry;
+
+		static HTAB *IndexAmRoutine_cache = NULL;
+
+		if (IndexAmRoutine_cache == NULL)
+		{
+			HASHCTL ctl;
+
+			/* initialize hash control structure */
+			MemSet(&ctl, 0, sizeof(ctl));
+			ctl.keysize = sizeof(Oid);
+			ctl.entrysize = sizeof(IndexAmRoutineCacheEntry);
+			ctl.hcxt = TopMemoryContext;
+
+			/* create cache in TopMemoryContext; small fixed initial size */
+			IndexAmRoutine_cache = hash_create("IndexAmRoutine cache",
+											   8,
+											   &ctl,
+											   HASH_ELEM | HASH_BLOBS);
+		}
+
+		/* call handler to get the runtime-provided struct (may be palloc'd) */
+		IndexAmRoutine *runtime_routine = GetIndexAmRoutine(amhandler);
+		bool		found;
+		IndexAmRoutineCacheEntry *entry;
+
+		/* find or create entry; key is the amhandler Oid */
+		entry = (IndexAmRoutineCacheEntry *) hash_search(IndexAmRoutine_cache,
+														 (void *)&amhandler,
+														 HASH_ENTER,
+														 &found);
+		if (!found)
+		{
+			/* copy runtime struct into persistent cache entry */
+			/* ensure we don't copy past the size of IndexAmRoutine */
+			memcpy(&entry->routine, runtime_routine, sizeof(IndexAmRoutine));
+		}
+
+		/*
+		 * Note: we intentionally do not pfree(runtime_routine) here. The
+		 * handler may have returned a pointer into static memory or memory
+		 * allocated in some context. Freeing it blindly may be unsafe. The
+		 * runtime allocation (if any) will be at most one small struct per
+		 * handler and is acceptable for this prototype. A more polished
+		 * implementation could detect and free an allocated pointer when
+		 * safe.
+		 */
+
+		/* return pointer to the cached copy (in TopMemoryContext) */
+		return &entry->routine;
+	}
 }
 
 
@@ -187,7 +256,7 @@ amvalidate(PG_FUNCTION_ARGS)
 
 	result = amroutine->amvalidate(opclassoid);
 
-	pfree(amroutine);
+	/* Previously we pfree(amroutine); but routine is now cached in TopMemoryContext. */
 
 	PG_RETURN_BOOL(result);
-}
+}
\ No newline at end of file
-- 
2.39.5

Reply via email to