test.diff
application/octet-stream
Filename: test.diff
Type: application/octet-stream
Part: 1
Patch
Format: unified
| File | + | − |
|---|---|---|
| buftable_bench/.gitignore | 2 | 0 |
| buftable_bench/instrumentation/buftable_bench_module/buftable_bench--1.0.sql | 17 | 0 |
| buftable_bench/instrumentation/buftable_bench_module/buftable_bench.c | 255 | 0 |
| buftable_bench/instrumentation/buftable_bench_module/buftable_bench.control | 4 | 0 |
| buftable_bench/instrumentation/buftable_bench_module/Makefile | 22 | 0 |
| buftable_bench/instrumentation/buftable_bench_module/meson.build | 22 | 0 |
| buftable_bench/README.md | 57 | 0 |
| buftable_bench/results/compare_probe.results.txt | 24 | 0 |
| buftable_bench/results/probe_summary.txt | 29 | 0 |
| buftable_bench/run.sh | 97 | 0 |
| buftable_bench/scripts/bench_probe.sh | 63 | 0 |
| buftable_bench/scripts/compare_probe.sh | 44 | 0 |
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 "==============================================================="