From 4916a0891b2e7176dee3c2a3a8018a4d174dd373 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Thu, 29 Jan 2026 05:03:55 +0900
Subject: [PATCH v5 5/5] WIP: Use dedicated interpreter for batched qual
 evaluation

Move batch-related opcodes (EEOP_SCAN_FETCHSOME_BATCH,
EEOP_QUAL_BATCH_INITMASK, EEOP_QUAL_BATCH_TERM) out of the main
ExecInterpExpr switch and into a dedicated ExecInterpQualBatch
function.

Adding opcodes to ExecInterpExpr may affect performance even when
they are not executed, possibly due to changes in register allocation,
jump table layout, or code size. Use a separate interpreter to avoid
any risk of impacting the existing per-tuple evaluation path.

The batched qual program has a simple linear structure (fetch ->
initmask -> term* -> done) that doesn't need computed goto dispatch
anyway.
---
 src/backend/executor/execExprInterp.c | 72 +++++++++++++++++----------
 src/backend/executor/nodeSeqscan.c    |  6 +--
 2 files changed, 46 insertions(+), 32 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 304c7f4e0fb..04a40ec932c 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -189,6 +189,8 @@ static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate
 															  int setno);
 static char *ExecGetJsonValueItemString(JsonbValue *item, bool *resnull);
 
+static Datum ExecInterpQualBatch(ExprState *state, ExprContext *econtext);
+
 /*
  * ScalarArrayOpExprHashEntry
  * 		Hash table entry type used during EEOP_HASHED_SCALARARRAYOP
@@ -266,6 +268,12 @@ ExecReadyInterpretedExpr(ExprState *state)
 	 */
 	state->evalfunc = ExecInterpExprStillValid;
 
+	if (state->batch_private)
+	{
+		state->evalfunc_private = (void *) ExecInterpQualBatch;
+		return;
+	}
+
 	/* DIRECT_THREADED should not already be set */
 	Assert((state->flags & EEO_FLAG_DIRECT_THREADED) == 0);
 
@@ -467,7 +475,6 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 	TupleTableSlot *scanslot;
 	TupleTableSlot *oldslot;
 	TupleTableSlot *newslot;
-	TupleBatch *scanbatch;
 
 	/*
 	 * This array has to be in the same order as enum ExprEvalOp.
@@ -594,9 +601,9 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 		&&CASE_EEOP_AGG_PRESORTED_DISTINCT_MULTI,
 		&&CASE_EEOP_AGG_ORDERED_TRANS_DATUM,
 		&&CASE_EEOP_AGG_ORDERED_TRANS_TUPLE,
-		&&CASE_EEOP_SCAN_FETCHSOME_BATCH,
-		&&CASE_EEOP_QUAL_BATCH_INITMASK,
-		&&CASE_EEOP_QUAL_BATCH_TERM,
+		&&CASE_EEOP_BATCH_UNREACHABLE,  /* EEOP_SCAN_FETCHSOME_BATCH */
+		&&CASE_EEOP_BATCH_UNREACHABLE,  /* EEOP_QUAL_BATCH_INITMASK */
+		&&CASE_EEOP_BATCH_UNREACHABLE,  /* EEOP_QUAL_BATCH_TERM */
 		&&CASE_EEOP_LAST
 	};
 
@@ -617,7 +624,6 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 	scanslot = econtext->ecxt_scantuple;
 	oldslot = econtext->ecxt_oldtuple;
 	newslot = econtext->ecxt_newtuple;
-	scanbatch = econtext->scan_batch;
 
 #if defined(EEO_USE_COMPUTED_GOTO)
 	EEO_DISPATCH();
@@ -2271,34 +2277,18 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
 			EEO_NEXT();
 		}
 
-		EEO_CASE(EEOP_SCAN_FETCHSOME_BATCH)
-		{
-			CheckOpSlotCompatibility(op, scanslot);
-
-			Assert(scanbatch);
-			slot_getsomeattrs_batch(scanbatch, op->d.fetch_batch.last_var);
-
-			EEO_NEXT();
-		}
-
-		EEO_CASE(EEOP_QUAL_BATCH_INITMASK)
-		{
-			ExecQualBatchInitMask(state, op, econtext);
-			EEO_NEXT();
-		}
-
-		EEO_CASE(EEOP_QUAL_BATCH_TERM)
-		{
-			ExecQualBatchTerm(state, op, econtext);
-			EEO_NEXT();
-		}
-
 		EEO_CASE(EEOP_LAST)
 		{
 			/* unreachable */
 			Assert(false);
 			goto out_error;
 		}
+
+		EEO_CASE(EEOP_BATCH_UNREACHABLE)
+		{
+			Assert(false && "batch opcodes use dedicated interpreter");
+			pg_unreachable();
+		}
 	}
 
 out_error:
@@ -6089,6 +6079,34 @@ ExecQualBatchTerm(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
 	}
 }
 
+static Datum
+ExecInterpQualBatch(ExprState *state, ExprContext *econtext)
+{
+	ExprEvalStep *op = state->steps;
+	TupleBatch *scanbatch = econtext->scan_batch;
+
+	/* Step 1: fetch/deform all slots */
+	Assert(ExecEvalStepOp(state, op) == EEOP_SCAN_FETCHSOME_BATCH);
+	slot_getsomeattrs_batch(scanbatch, op->d.fetch_batch.last_var);
+	op++;
+
+	/* Step 2: initialize mask */
+	Assert(ExecEvalStepOp(state, op) == EEOP_QUAL_BATCH_INITMASK);
+	ExecQualBatchInitMask(state, op, econtext);
+	op++;
+
+	/* Step 3: process all TERM steps */
+	while (ExecEvalStepOp(state, op) == EEOP_QUAL_BATCH_TERM)
+	{
+		ExecQualBatchTerm(state, op, econtext);
+		op++;
+	}
+
+	Assert(ExecEvalStepOp(state, op) == EEOP_DONE_NO_RETURN);
+
+	return (Datum) 0;
+}
+
 /*
  * ExecQualBatch
  *		Evaluate a batched qual over all rows in a TupleBatch.
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 16f15ed68aa..4a76108bd2f 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -404,7 +404,6 @@ SeqScanState *
 ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 {
 	SeqScanState *scanstate;
-	bool	use_batching;
 
 	/*
 	 * Once upon a time it was possible to have an outerPlan of a SeqScan, but
@@ -435,12 +434,9 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 							 node->scan.scanrelid,
 							 eflags);
 
-	use_batching = ScanCanUseBatching(&scanstate->ss, eflags);
-
 	/* and create slot with the appropriate rowtype */
 	ExecInitScanTupleSlot(estate, &scanstate->ss,
 						  RelationGetDescr(scanstate->ss.ss_currentRelation),
-						  use_batching ? &TTSOpsHeapTuple :
 						  table_slot_callbacks(scanstate->ss.ss_currentRelation));
 
 	/*
@@ -477,7 +473,7 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 			scanstate->ss.ps.ExecProcNode = ExecSeqScanWithQualProject;
 	}
 
-	if (use_batching)
+	if (ScanCanUseBatching(&scanstate->ss, eflags))
 		SeqScanInitBatching(scanstate, eflags);
 
 	return scanstate;
-- 
2.47.3

