diff --git a/buftable_bench/.gitignore b/buftable_bench/.gitignore
new file mode 100644
index 00000000000..2988e8c3b49
--- /dev/null
+++ b/buftable_bench/.gitignore
@@ -0,0 +1,2 @@
+_work/
+.builds/
diff --git a/buftable_bench/README.md b/buftable_bench/README.md
new file mode 100644
index 00000000000..b3b92a38a35
--- /dev/null
+++ b/buftable_bench/README.md
@@ -0,0 +1,57 @@
+# buftable_bench
+
+A micro-benchmark of PostgreSQL's shared **buffer mapping table** ops. The SQL function
+`buftable_bench_probe(n, rounds, random)` calls `BufTableLookup` / `BufTableInsert` /
+`BufTableDelete` **directly** on the live shared table and **bulk-times** each op (one rdtsc pair ÷
+N) — no `ReadBuffer`, no page copy, no per-op rdtsc. It operates on free buffer slots + synthetic
+tags and restores the table afterward.
+
+The whole thing is one self-contained test-module extension — the **only change vs stock
+PostgreSQL**. x86_64 only (uses `rdtsc`).
+
+## Files
+
+```
+buftable_bench/
+├── run.sh                                   # build the current branch (release) + run + print numbers
+├── instrumentation/buftable_bench_module/   # the extension (drop into src/test/modules/)
+│   ├── buftable_bench.c                      #   buftable_bench_probe() — the probe
+│   ├── buftable_bench--1.0.sql               #   CREATE FUNCTION buftable_bench_probe(...)
+│   ├── buftable_bench.control
+│   ├── Makefile
+│   └── meson.build
+├── scripts/
+│   ├── bench_probe.sh   <prefix> <size>      # run the probe against one explicit install
+│   └── compare_probe.sh [sizes...]           # flat-vs-dynahash A/B (needs two builds)
+└── results/
+    ├── probe_summary.txt                     # recorded A/B numbers
+    └── compare_probe.results.txt
+```
+(`.builds/` and `_work/` are local build/scratch caches — gitignored.)
+
+## How to run
+
+**The current branch** (builds this repo's `HEAD` as a release, caches it per commit, runs, and
+prints the per-op numbers):
+
+```sh
+buftable_bench/run.sh                 # default shared_buffers = 1GB
+buftable_bench/run.sh 256MB 4GB 16GB  # any sizes
+```
+First run for a commit builds (~30 s–few min); later runs are instant. Output (to stdout), labeled
+`branch@commit`:
+```
+-- shared_buffers=1GB  (n=104857 keys) --
+     op      | avg_ns |  count
+ insert      | 18.555 | 1048570
+ lookup_hit  | 23.064 | 1048570
+ lookup_miss | 21.902 | 1048570
+ delete      | 24.569 | 1048570
+```
+Knobs: `BUFTABLE_PROBE_ROUNDS` (default 10); `PG_CONFIG=/path/bin/pg_config` to skip the build and
+use an existing install. (`run.sh` is **single-arm** — it measures the current branch only.)
+
+**Flat-vs-dynahash A/B** (optional; needs two installs at `~/pg-bench/{flat,dyna}`):
+```sh
+buftable_bench/scripts/compare_probe.sh 256MB 4GB 16GB
+```
diff --git a/buftable_bench/instrumentation/buftable_bench_module/Makefile b/buftable_bench/instrumentation/buftable_bench_module/Makefile
new file mode 100644
index 00000000000..75ca30c79fe
--- /dev/null
+++ b/buftable_bench/instrumentation/buftable_bench_module/Makefile
@@ -0,0 +1,22 @@
+# src/test/modules/buftable_bench/Makefile
+
+PGFILEDESC = "buftable_bench - rdtsc micro-benchmark for the buffer mapping table"
+
+MODULE_big = buftable_bench
+OBJS = \
+	$(WIN32RES) \
+	buftable_bench.o
+
+EXTENSION = buftable_bench
+DATA = buftable_bench--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/buftable_bench
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/buftable_bench/instrumentation/buftable_bench_module/buftable_bench--1.0.sql b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench--1.0.sql
new file mode 100644
index 00000000000..f81c0e60d5d
--- /dev/null
+++ b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench--1.0.sql
@@ -0,0 +1,17 @@
+/* src/test/modules/buftable_bench/buftable_bench--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION buftable_bench" to load this file. \quit
+
+CREATE FUNCTION buftable_bench_probe(
+	IN n int8,
+	IN rounds int8 DEFAULT 1,
+	IN random bool DEFAULT true,
+	OUT op text,
+	OUT avg_ns float8,
+	OUT count int8)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'buftable_bench_probe'
+LANGUAGE C;
+
+REVOKE ALL ON FUNCTION buftable_bench_probe(int8, int8, bool) FROM PUBLIC;
diff --git a/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.c b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.c
new file mode 100644
index 00000000000..67d09b3579c
--- /dev/null
+++ b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.c
@@ -0,0 +1,255 @@
+/*-------------------------------------------------------------------------
+ *
+ * buftable_bench.c
+ *		Pollution-free in-place benchmark of the shared buffer mapping table.
+ *
+ * Throwaway micro-benchmark module (NOT for upstream).  One SQL function,
+ * buftable_bench_probe(n, rounds), times lookup (hit+miss), insert, and delete
+ * by calling BufTable{Insert,Lookup,Delete} DIRECTLY on the real shared table
+ * -- no ReadBuffer, no 8 KB page copy, no per-op rdtsc.  Each op's loop is
+ * bulk-timed with a single rdtsc pair, so the measurement isn't polluted by
+ * page-copy cache traffic or per-op timer overhead.
+ *
+ * It works against STOCK PostgreSQL: it only calls the existing public
+ * BufTable* / BufTableHashCode functions, so no core changes are needed -- the
+ * two arms being compared are just two stock builds (flat table vs dynahash).
+ *
+ * Insert/delete mutate the live table, so we only use FREE buffer slots (their
+ * mapping entry is guaranteed empty) and restore the table afterward.
+ *
+ * x86_64 only (rdtsc).
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/buftable_bench/buftable_bench.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "portability/instr_time.h"
+#include "storage/buf_internals.h"
+#include "storage/bufmgr.h"
+#include "utils/builtins.h"
+#include "utils/tuplestore.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(buftable_bench_probe);
+
+#define BUFTABLE_BENCH_PROBE_COLS 3
+
+/* synthetic relfilenodes for bench tags — unlikely to collide with anything real */
+#define BENCH_SPC_OID  0xB0B0
+#define BENCH_DB_OID   0xB1B1
+#define BENCH_REL_PRESENT  ((RelFileNumber) 0x7E570001)
+#define BENCH_REL_ABSENT   ((RelFileNumber) 0x7E570002)
+
+static inline uint64
+bench_rdtsc(void)
+{
+	uint32		lo,
+				hi;
+
+	__asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi)::"memory");
+	return ((uint64) hi << 32) | lo;
+}
+
+/* cycles per nanosecond, measured over a ~2 ms wall-clock window */
+static double
+probe_calibrate(void)
+{
+	instr_time	w0,
+				w1,
+				d;
+	uint64		c0,
+				c1;
+	double		ns;
+
+	INSTR_TIME_SET_CURRENT(w0);
+	c0 = bench_rdtsc();
+	do
+	{
+		INSTR_TIME_SET_CURRENT(w1);
+		d = w1;
+		INSTR_TIME_SUBTRACT(d, w0);
+	} while (INSTR_TIME_GET_DOUBLE(d) < 0.002);
+	c1 = bench_rdtsc();
+	ns = INSTR_TIME_GET_DOUBLE(d) * 1e9;
+	return (ns > 0.0) ? (double) (c1 - c0) / ns : 0.0;
+}
+
+/*
+ * buftable_bench_probe(n, rounds) -> SETOF (op text, avg_ns float8, count int8)
+ *
+ * Rows: insert, lookup_hit, lookup_miss, delete.  See file header.
+ */
+Datum
+buftable_bench_probe(PG_FUNCTION_ARGS)
+{
+	int64		n = PG_GETARG_INT64(0);
+	int64		rounds = PG_ARGISNULL(1) ? 1 : PG_GETARG_INT64(1);
+	bool		randomize = PG_ARGISNULL(2) ? true : PG_GETARG_BOOL(2);
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	int		   *bufids;
+	int64	   *ord;
+	BufferTag  *ptag,
+			   *atag;
+	uint32	   *phash,
+			   *ahash;
+	int64		nfree = 0;
+	double		cyc_per_ns,
+				denom;
+	uint64		ins = 0,
+				lkh = 0,
+				lkm = 0,
+				del = 0;
+	volatile int64 sink = 0;
+	RelFileLocator rp = {.spcOid = BENCH_SPC_OID,.dbOid = BENCH_DB_OID,.relNumber = BENCH_REL_PRESENT};
+	RelFileLocator ra = {.spcOid = BENCH_SPC_OID,.dbOid = BENCH_DB_OID,.relNumber = BENCH_REL_ABSENT};
+	const char *names[4] = {"insert", "lookup_hit", "lookup_miss", "delete"};
+	double		avg[4];
+
+	if (n <= 0 || rounds <= 0)
+		ereport(ERROR, (errmsg("n and rounds must be positive")));
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	/* collect up to n FREE buffer slots (mapping entry guaranteed empty) */
+	bufids = palloc(sizeof(int) * n);
+	for (int i = 0; i < NBuffers && nfree < n; i++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(i);
+		uint64		state = pg_atomic_read_u64(&desc->state);
+
+		if (!(state & BM_TAG_VALID))
+			bufids[nfree++] = i;
+	}
+	n = nfree;
+	if (n == 0)
+		ereport(ERROR, (errmsg("no free buffers to probe with")));
+
+	/* build present + absent tags and their hashes */
+	ptag = palloc(sizeof(BufferTag) * n);
+	atag = palloc(sizeof(BufferTag) * n);
+	phash = palloc(sizeof(uint32) * n);
+	ahash = palloc(sizeof(uint32) * n);
+	for (int64 j = 0; j < n; j++)
+	{
+		InitBufferTag(&ptag[j], &rp, MAIN_FORKNUM, (BlockNumber) j);
+		InitBufferTag(&atag[j], &ra, MAIN_FORKNUM, (BlockNumber) j);
+		phash[j] = BufTableHashCode(&ptag[j]);
+		ahash[j] = BufTableHashCode(&atag[j]);
+	}
+
+	/*
+	 * Iteration order over the keys: identity (sequential) or a Fisher-Yates
+	 * shuffle (random).  A shuffled order makes the timed loops visit keys in
+	 * an order uncorrelated with where their entries/elements live, so BOTH
+	 * arms' entry/element access is random (not just the bucket access, which
+	 * the hash already scatters).  Done once in setup (untimed).
+	 */
+	ord = palloc(sizeof(int64) * n);
+	for (int64 i = 0; i < n; i++)
+		ord[i] = i;
+	if (randomize)
+	{
+		uint64		rng = 0x9E3779B97F4A7C15ULL;	/* fixed seed -> reproducible */
+
+		for (int64 i = n - 1; i > 0; i--)
+		{
+			int64		k,
+						tmp;
+
+			rng ^= rng << 13;
+			rng ^= rng >> 7;
+			rng ^= rng << 17;
+			k = (int64) (rng % (uint64) (i + 1));
+			tmp = ord[i];
+			ord[i] = ord[k];
+			ord[k] = tmp;
+		}
+	}
+
+	cyc_per_ns = probe_calibrate();
+
+	PG_TRY();
+	{
+		for (int64 r = 0; r < rounds; r++)
+		{
+			uint64		t0,
+						t1;
+
+			t0 = bench_rdtsc();
+			for (int64 i = 0; i < n; i++)
+			{
+				int64		j = ord[i];
+
+				BufTableInsert(&ptag[j], phash[j], bufids[j]);
+			}
+			t1 = bench_rdtsc();
+			ins += t1 - t0;
+
+			t0 = bench_rdtsc();
+			for (int64 i = 0; i < n; i++)
+			{
+				int64		j = ord[i];
+
+				sink += BufTableLookup(&ptag[j], phash[j]);
+			}
+			t1 = bench_rdtsc();
+			lkh += t1 - t0;
+
+			t0 = bench_rdtsc();
+			for (int64 i = 0; i < n; i++)
+			{
+				int64		j = ord[i];
+
+				sink += BufTableLookup(&atag[j], ahash[j]);
+			}
+			t1 = bench_rdtsc();
+			lkm += t1 - t0;
+
+			t0 = bench_rdtsc();
+			for (int64 i = 0; i < n; i++)
+			{
+				int64		j = ord[i];
+
+				BufTableDelete(&ptag[j], phash[j]);
+			}
+			t1 = bench_rdtsc();
+			del += t1 - t0;
+		}
+	}
+	PG_CATCH();
+	{
+		/* best-effort restore: remove any present tag still mapped */
+		for (int64 j = 0; j < n; j++)
+			if (BufTableLookup(&ptag[j], phash[j]) >= 0)
+				BufTableDelete(&ptag[j], phash[j]);
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	denom = (double) n * (double) rounds * cyc_per_ns;
+	avg[0] = ins / denom;
+	avg[1] = lkh / denom;
+	avg[2] = lkm / denom;
+	avg[3] = del / denom;
+
+	for (int i = 0; i < 4; i++)
+	{
+		Datum		values[BUFTABLE_BENCH_PROBE_COLS];
+		bool		nulls[BUFTABLE_BENCH_PROBE_COLS] = {0};
+
+		values[0] = CStringGetTextDatum(names[i]);
+		values[1] = Float8GetDatum(avg[i]);
+		values[2] = Int64GetDatum(n * rounds);
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	(void) sink;
+	return (Datum) 0;
+}
diff --git a/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.control b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.control
new file mode 100644
index 00000000000..aae6cb1810f
--- /dev/null
+++ b/buftable_bench/instrumentation/buftable_bench_module/buftable_bench.control
@@ -0,0 +1,4 @@
+comment = 'rdtsc micro-benchmark for the shared buffer mapping table'
+default_version = '1.0'
+module_pathname = '$libdir/buftable_bench'
+relocatable = true
diff --git a/buftable_bench/instrumentation/buftable_bench_module/meson.build b/buftable_bench/instrumentation/buftable_bench_module/meson.build
new file mode 100644
index 00000000000..752773841b2
--- /dev/null
+++ b/buftable_bench/instrumentation/buftable_bench_module/meson.build
@@ -0,0 +1,22 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+buftable_bench_sources = files(
+  'buftable_bench.c',
+)
+
+if host_system == 'windows'
+  buftable_bench_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'buftable_bench',
+    '--FILEDESC', 'buftable_bench - rdtsc micro-benchmark for the buffer mapping table',])
+endif
+
+buftable_bench = shared_module('buftable_bench',
+  buftable_bench_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += buftable_bench
+
+test_install_data += files(
+  'buftable_bench.control',
+  'buftable_bench--1.0.sql',
+)
diff --git a/buftable_bench/results/compare_probe.results.txt b/buftable_bench/results/compare_probe.results.txt
new file mode 100644
index 00000000000..c55903132b8
--- /dev/null
+++ b/buftable_bench/results/compare_probe.results.txt
@@ -0,0 +1,24 @@
+RESULT dyna 256MB insert 28.995 262140
+RESULT dyna 256MB lookup_hit 21.134 262140
+RESULT dyna 256MB lookup_miss 21.257 262140
+RESULT dyna 256MB delete 21.798 262140
+RESULT flat 256MB insert 13.245 262140
+RESULT flat 256MB lookup_hit 16.222 262140
+RESULT flat 256MB lookup_miss 17.498 262140
+RESULT flat 256MB delete 17.805 262140
+RESULT dyna 4GB insert 66.845 4194300
+RESULT dyna 4GB lookup_hit 49.673 4194300
+RESULT dyna 4GB lookup_miss 39.825 4194300
+RESULT dyna 4GB delete 56.704 4194300
+RESULT flat 4GB insert 30.506 4194300
+RESULT flat 4GB lookup_hit 31.717 4194300
+RESULT flat 4GB lookup_miss 28.786 4194300
+RESULT flat 4GB delete 34.971 4194300
+RESULT dyna 16GB insert 104.300 16777210
+RESULT dyna 16GB lookup_hit 82.074 16777210
+RESULT dyna 16GB lookup_miss 62.069 16777210
+RESULT dyna 16GB delete 88.639 16777210
+RESULT flat 16GB insert 55.068 16777210
+RESULT flat 16GB lookup_hit 62.942 16777210
+RESULT flat 16GB lookup_miss 47.260 16777210
+RESULT flat 16GB delete 69.308 16777210
diff --git a/buftable_bench/results/probe_summary.txt b/buftable_bench/results/probe_summary.txt
new file mode 100644
index 00000000000..a6ac9ba855d
--- /dev/null
+++ b/buftable_bench/results/probe_summary.txt
@@ -0,0 +1,29 @@
+# Pollution-free in-place A/B of the REAL shared buffer mapping table.
+# buftable_bench_probe(n, rounds, random): direct BufTable{Insert,Lookup,Delete}
+# calls, bulk-timed (one rdtsc pair per phase), NO ReadBuffer/page-copy, NO
+# per-op rdtsc.  random=true (DEFAULT): keys visited in a shuffled order so the
+# bucket AND entry/element accesses are random for both arms.
+# speedup = dynahash/flat (>1 = flat faster). n ~ 0.8*NBuffers, rounds=10.
+#
+# === RANDOM access (default) ===
+# sb     op            dynahash_ns  flat_ns  speedup
+# 256MB  insert            28.99     13.25    2.19x
+# 256MB  lookup_hit        21.13     16.22    1.30x
+# 256MB  lookup_miss       21.26     17.50    1.21x
+# 256MB  delete            21.80     17.81    1.22x
+# 4GB    insert            66.85     30.51    2.19x
+# 4GB    lookup_hit        49.67     31.72    1.57x
+# 4GB    lookup_miss       39.83     28.79    1.38x
+# 4GB    delete            56.70     34.97    1.62x
+# 16GB   insert           104.30     55.07    1.89x
+# 16GB   lookup_hit        82.07     62.94    1.30x
+# 16GB   lookup_miss       62.07     47.26    1.31x
+# 16GB   delete            88.64     69.31    1.28x
+#
+# Flat is FASTER on EVERY op at EVERY size (1.2-2.2x). Random access raises
+# absolute ns and compresses the ratio vs sequential (a common-mode key-fetch
+# cost, equal for both arms) -- the more conservative, realistic number.
+#
+# For reference, the earlier SEQUENTIAL order (random=false) gave larger ratios:
+#   insert 2.4/2.8/3.5x, lookup_hit 1.6/1.7/2.3x, lookup_miss 1.3/1.3/1.7x,
+#   delete 1.4/1.6/2.2x @256MB/4GB/16GB (entry/element access was prefetch-friendly).
diff --git a/buftable_bench/run.sh b/buftable_bench/run.sh
new file mode 100755
index 00000000000..5ac38801863
--- /dev/null
+++ b/buftable_bench/run.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+#
+# run.sh [size ...]
+#
+# Build the CURRENTLY CHECKED-OUT BRANCH of this repo (its HEAD) as a release,
+# then run the buffer-mapping-table probe and print the per-op numbers for it.
+# Single arm (just this branch's buf_table.c) — for the flat-vs-dynahash A/B
+# use scripts/compare_probe.sh.
+#
+# The build is cached per commit under buftable_bench/.builds/<commit>, so the
+# first run for a commit takes a few minutes and later runs are instant.
+# It exports the committed tree (git archive) into a temp dir to build, so your
+# working checkout is never touched.
+#
+# Env: PG_CONFIG (use this prebuilt install instead of building),
+#      BUFTABLE_PROBE_ROUNDS (default 10), BUFTABLE_BENCH_WORK.
+set -euo pipefail
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+MODSRC="$HERE/instrumentation/buftable_bench_module"
+WORK="${BUFTABLE_BENCH_WORK:-$HERE/_work}"; mkdir -p "$WORK"
+ROUNDS="${BUFTABLE_PROBE_ROUNDS:-10}"
+SIZES="${*:-1GB}"
+
+repo="$(git -C "$HERE" rev-parse --show-toplevel)"
+commit="$(git -C "$repo" rev-parse --short HEAD)"
+branch="$(git -C "$repo" rev-parse --abbrev-ref HEAD)"
+label="$branch@$commit"
+
+# ---- locate or build a release install of the current HEAD ------------------
+if [[ -n "${PG_CONFIG:-}" ]]; then
+	BIN="$("$PG_CONFIG" --bindir)"
+	echo "==> using PG_CONFIG build: $("$PG_CONFIG" --version) [$BIN]" >&2
+else
+	prefix="$HERE/.builds/$commit"
+	if [[ -x "$prefix/bin/pg_config" ]] && \
+	   [[ -e "$("$prefix/bin/pg_config" --pkglibdir)/buftable_bench.so" ]]; then
+		echo "==> reusing cached release build of $label  [$prefix]" >&2
+	else
+		echo "==> building $label as release (first run for this commit, ~2-4 min)..." >&2
+		src="$(mktemp -d "$WORK/src.$commit.XXXX")"
+		git -C "$repo" archive HEAD | tar -x -C "$src"
+		mkdir -p "$src/src/test/modules/buftable_bench"
+		cp "$MODSRC"/* "$src/src/test/modules/buftable_bench"/
+		(
+			cd "$src"
+			./configure --prefix="$prefix" --without-icu --without-zlib --without-readline >/dev/null
+			make -s -j"$(nproc)" install >/dev/null
+			make -s -C src/test/modules/buftable_bench install >/dev/null
+		) || { echo "build failed; see $src" >&2; exit 1; }
+		rm -rf "$src"
+		echo "==> built $label" >&2
+	fi
+	BIN="$prefix/bin"
+fi
+
+# ---- run the probe per size -------------------------------------------------
+size_to_bytes() {
+	local s="${1^^}"
+	case "$s" in
+		*GB) echo $(( ${s%GB} * 1024 * 1024 * 1024 ));;
+		*MB) echo $(( ${s%MB} * 1024 * 1024 ));;
+		*KB) echo $(( ${s%KB} * 1024 ));;
+		*)   echo "$s";;
+	esac
+}
+
+echo
+echo "===== buftable_bench: $label (random access, rounds=$ROUNDS) ====="
+for SIZE in $SIZES; do
+	DATADIR="$(mktemp -d "$WORK/pgdata.${SIZE}.XXXX")"
+	SOCKDIR="$(mktemp -d /tmp/pgb.XXXXXX)"
+	N="$(awk -v b="$(size_to_bytes "$SIZE")" 'BEGIN{printf "%d", 0.8*b/8192}')"
+
+	"$BIN/initdb" -D "$DATADIR" --no-sync -A trust >/dev/null 2>&1
+	cat >> "$DATADIR/postgresql.conf" <<CONF
+shared_buffers = '$SIZE'
+jit = off
+autovacuum = off
+fsync = off
+bgwriter_lru_maxpages = 0
+listen_addresses = ''
+unix_socket_directories = '$SOCKDIR'
+CONF
+	"$BIN/pg_ctl" -D "$DATADIR" -l "$DATADIR/log" -w start >/dev/null
+
+	echo "-- shared_buffers=$SIZE  (n=$N keys) --"
+	"$BIN/psql" -h "$SOCKDIR" -d postgres -q -P pager=off \
+		-c "CREATE EXTENSION buftable_bench;" \
+		-c "SELECT op, round(avg_ns::numeric,3) AS avg_ns, count
+		    FROM buftable_bench_probe($N, $ROUNDS)
+		    ORDER BY array_position(ARRAY['insert','lookup_hit','lookup_miss','delete'], op);"
+
+	"$BIN/pg_ctl" -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true
+	rm -rf "$DATADIR" "$SOCKDIR"
+done
+echo "================================================================"
diff --git a/buftable_bench/scripts/bench_probe.sh b/buftable_bench/scripts/bench_probe.sh
new file mode 100755
index 00000000000..fde619911b2
--- /dev/null
+++ b/buftable_bench/scripts/bench_probe.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+#
+# bench_probe.sh <pg_install_prefix> <shared_buffers_size>
+#
+# Pollution-free, in-place benchmark of the REAL shared buffer mapping table for
+# lookup (hit+miss), insert, and delete, via buftable_bench_probe() — which calls
+# BufTable{Insert,Lookup,Delete} directly (no ReadBuffer, no 8 KB page copy, no
+# per-op rdtsc; each op's loop is bulk-timed).  No table/prewarm needed: it uses
+# free buffer slots + synthetic tags and restores the table afterward.
+#
+# Emits "RESULT <arm> <size> <op> <avg_ns> <count>".
+# Env: BUFTABLE_PROBE_ROUNDS (default 10).
+set -euo pipefail
+
+PREFIX="${1:?usage: bench_probe.sh <prefix> <size>}"
+SIZE="${2:?usage: bench_probe.sh <prefix> <size>}"
+ROUNDS="${BUFTABLE_PROBE_ROUNDS:-10}"
+ARM="$(basename "$PREFIX")"
+BIN="$PREFIX/bin"
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SCRATCH="${BUFTABLE_BENCH_WORK:-$HERE/_work}"; mkdir -p "$SCRATCH"
+DATADIR="$(mktemp -d "$SCRATCH/pgdata.${ARM}.${SIZE}.pr.XXXX")"
+SOCKDIR="$(mktemp -d /tmp/pgb.XXXXXX)"
+LOG="$DATADIR/server.log"
+
+log() { echo "[$ARM $SIZE probe] $*" >&2; }
+cleanup() { "$BIN/pg_ctl" -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true; rm -rf "$DATADIR" "$SOCKDIR"; }
+trap cleanup EXIT
+
+size_to_bytes() {
+	local s="${1^^}"
+	case "$s" in
+		*GB) echo $(( ${s%GB} * 1024 * 1024 * 1024 ));;
+		*MB) echo $(( ${s%MB} * 1024 * 1024 ));;
+		*)   echo "$s";;
+	esac
+}
+SB="$(size_to_bytes "$SIZE")"
+N="$(awk -v b="$SB" 'BEGIN{printf "%d", 0.8*b/8192}')"   # ~0.8x NBuffers -> load factor ~1
+
+"$BIN/initdb" -D "$DATADIR" --no-sync -A trust >/dev/null 2>&1
+cat >> "$DATADIR/postgresql.conf" <<CONF
+shared_buffers = '$SIZE'
+jit = off
+autovacuum = off
+fsync = off
+bgwriter_lru_maxpages = 0
+listen_addresses = ''
+unix_socket_directories = '$SOCKDIR'
+CONF
+
+log "start (shared_buffers=$SIZE, n=$N, rounds=$ROUNDS)"
+"$BIN/pg_ctl" -D "$DATADIR" -l "$LOG" -w start >/dev/null
+
+OUT="$("$BIN/psql" -h "$SOCKDIR" -d postgres -q -X -At -F' ' -v ON_ERROR_STOP=1 \
+	-c "CREATE EXTENSION buftable_bench;" \
+	-c "SELECT 'R', op, round(avg_ns::numeric,3), count FROM buftable_bench_probe($N, $ROUNDS);" 2>&1)" || {
+	log "psql failed:"; echo "$OUT" >&2; exit 1;
+}
+
+echo "$OUT" | awk -v arm="$ARM" -v sz="$SIZE" '$1=="R"{printf "RESULT %s %s %s %s %s\n", arm, sz, $2, $3, $4}'
+log "done"
diff --git a/buftable_bench/scripts/compare_probe.sh b/buftable_bench/scripts/compare_probe.sh
new file mode 100755
index 00000000000..fe98a8ab149
--- /dev/null
+++ b/buftable_bench/scripts/compare_probe.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+#
+# compare_probe.sh [size ...]
+#
+# Pollution-free in-place A/B of the shared buffer mapping table for all three
+# ops (insert / lookup_hit / lookup_miss / delete), via buftable_bench_probe().
+# Prints, per size, dynahash vs flat avg_ns and speedup = dynahash/flat (>1 =
+# flat faster).  Sizes default "256MB 4GB 16GB" or $BUFTABLE_CAPI_SIZES or args.
+set -euo pipefail
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BENCH="$HERE/bench_probe.sh"
+FLAT=/home/dhruv.aron/pg-bench/flat
+DYNA=/home/dhruv.aron/pg-bench/dyna
+SCRATCH="${BUFTABLE_BENCH_WORK:-$HERE/_work}"; mkdir -p "$SCRATCH"
+RESULTS="$SCRATCH/compare_probe.results.txt"; : > "$RESULTS"
+
+if [[ $# -gt 0 ]]; then SIZES="$*"; else SIZES="${BUFTABLE_CAPI_SIZES:-256MB 4GB 16GB}"; fi
+
+for size in $SIZES; do
+	for prefix in "$DYNA" "$FLAT"; do
+		echo ">>> $(basename "$prefix") @ $size" >&2
+		bash "$BENCH" "$prefix" "$size" | grep '^RESULT ' >> "$RESULTS"
+	done
+done
+
+echo
+echo "===== pollution-free direct-probe A/B (real shared table) ====="
+awk '
+{ arm=$2; size=$3; op=$4; v[size,op,arm]=$5; cnt[size,op,arm]=$6;
+  if(!(size in seen)){seen[size]=1; order[++ni]=size} }
+END{
+  split("insert lookup_hit lookup_miss delete", ops, " ");
+  for(i=1;i<=ni;i++){ s=order[i];
+    printf "\n=== shared_buffers = %s  (samples/op %s) ===\n", s, cnt[s,"insert","flat"];
+    printf "%-12s %12s %12s %14s\n","op","dynahash ns","flat ns","speedup(dh/ft)";
+    for(j=1;j<=4;j++){ o=ops[j];
+      dh=v[s,o,"dyna"]; ft=v[s,o,"flat"];
+      if(dh==""||ft==""){ printf "%-12s %12s %12s %14s\n",o,(dh==""?"-":dh),(ft==""?"-":ft),"n/a"; continue }
+      printf "%-12s %12.3f %12.3f %13.2fx\n", o, dh, ft, (ft>0?dh/ft:0);
+    }
+  }
+}' "$RESULTS"
+echo "==============================================================="
