Hi Dhruv,
I am formatting your patch, and doing a few minor adjustments
It addresses Andres Freund's feedback
> FWIW, on machines with the necessary hardware support, postgres'
instr_time.h
> should now be quite fast, it's using rdtsc[p] if available.
This version uses instr_time and a a little bit of pre-processing to write
the
benchmark as
+ BEGIN_TIMING("insert", n)
+ int32 j = w_ord[i];
+ BufTableInsert(&ptag[j], phash[j], bufids[j]);
+ END_TIMING
+
+ BEGIN_TIMING("hit", n)
+ int32 j = r_ord[i];
+ sink += BufTableLookup(&ptag[j], phash[j]);
+ END_TIMING
I also increased the number of buckets to make 1/3 < occupation < 2/3
instead of 1/2 < occupation < 1. 100% occupation could produce longer
chains (not critical though, as it is not using open addressing).
> I don't really understand the race condition this is trying to address:
>
>> + /* Unlock buffer header after the entry is deleted to avoid a race
condition:
>> + * If unlocked prior, a concurrent GetVictimBuffer() could insert
a new entry
>> + * for the same buffer and overwrite the entry slot. Then, the
BufTableDelete()
>> + * would be unable to find the entry and would corrupt the
hashtable. */
>> + UnlockBufHdrExt(buf, buf_state,
>> + 0,
>> + BUF_FLAG_MASK | BUF_USAGECOUNT_MASK,
>> + 0);
>
>How could there be a concurrent insertion while the buffer partition lock
is
>held?
I am removing that part as there is a consensus among the big guys that
we shouldn't hold a spin-lock while doing the BufferTableDelete.
I think I could explain why this is necessary, but I want to see your
argument.
Was it safe before? if so, what is the property of dynhash that we lost on
this
patch.
I could argue that BufferTableDelete is safe if we check for the bounds of
.next and make sure there are no cycles in the chain. But again I will let
you elaborate that argument.
Regards,
Alexandre
From 62fd0eea23eab846bed6801017d4ca4a33094b70 Mon Sep 17 00:00:00 2001
From: Dhruv Aron <[email protected]>
Date: Wed, 8 Jul 2026 17:29:00 +0100
Subject: [PATCH] Buffer table test module
This module implements a function buftable_bench_probe that
measures times of inserting, searching and deleting all of them
repeatetedly, and returns each measurement as a row.
This uses "portability/instr_time.h" for time measurement,
Analysis can be performed using SQL e.g.
CREATE EXTENSION buftable_bench;
SELECT rand, op,
avg(avg_ns) AS avg,
min(avg_ns) AS min,
percentile_disc(0.25) WITHIN GROUP (ORDER BY avg_ns) AS \"[q1\",
percentile_disc(0.50) WITHIN GROUP (ORDER BY avg_ns) AS median,
percentile_disc(0.75) WITHIN GROUP (ORDER BY avg_ns) AS \"q3]\",
percentile_disc(0.99) WITHIN GROUP (ORDER BY avg_ns) AS p99,
stddev(avg_ns) AS std
FROM
unnest(ARRAY[false, true]) rand(rand),
LATERAL buftable_bench_probe($N, $ROUNDS, rand)
GROUP BY rand, op, id
ORDER BY id, rand;
---
src/test/modules/buftable_bench/Makefile | 23 ++
.../buftable_bench/buftable_bench--1.0.sql | 19 ++
.../modules/buftable_bench/buftable_bench.c | 223 ++++++++++++++++++
.../buftable_bench/buftable_bench.control | 4 +
src/test/modules/buftable_bench/meson.build | 23 ++
src/test/modules/buftable_bench/no_optimise.c | 27 +++
src/test/modules/buftable_bench/no_optimise.h | 16 ++
7 files changed, 335 insertions(+)
create mode 100644 src/test/modules/buftable_bench/Makefile
create mode 100644 src/test/modules/buftable_bench/buftable_bench--1.0.sql
create mode 100644 src/test/modules/buftable_bench/buftable_bench.c
create mode 100644 src/test/modules/buftable_bench/buftable_bench.control
create mode 100644 src/test/modules/buftable_bench/meson.build
create mode 100644 src/test/modules/buftable_bench/no_optimise.c
create mode 100644 src/test/modules/buftable_bench/no_optimise.h
diff --git a/src/test/modules/buftable_bench/Makefile b/src/test/modules/buftable_bench/Makefile
new file mode 100644
index 00000000000..1f968496cf2
--- /dev/null
+++ b/src/test/modules/buftable_bench/Makefile
@@ -0,0 +1,23 @@
+# 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 \
+ no_optimise.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/src/test/modules/buftable_bench/buftable_bench--1.0.sql b/src/test/modules/buftable_bench/buftable_bench--1.0.sql
new file mode 100644
index 00000000000..468259d8190
--- /dev/null
+++ b/src/test/modules/buftable_bench/buftable_bench--1.0.sql
@@ -0,0 +1,19 @@
+/* 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 batch_size int8,
+ OUT id 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/src/test/modules/buftable_bench/buftable_bench.c b/src/test/modules/buftable_bench/buftable_bench.c
new file mode 100644
index 00000000000..5575f264183
--- /dev/null
+++ b/src/test/modules/buftable_bench/buftable_bench.c
@@ -0,0 +1,223 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 timing. Each op's loop is
+ * bulk-timed with a single instr_time pair (RDTSC on x86 when available), 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"
+#include "common/pg_prng.h"
+
+#include "no_optimise.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(buftable_bench_probe);
+
+/*
+ * Auxiliary macros for readability
+*/
+static int64 timing_operation_id = 0;
+#define BEGIN_TIMING(name, n) \
+{\
+ instr_time t0, t1, dt; \
+ Datum values[4]; \
+ bool nulls[4] = {0}; \
+ values[0] = CStringGetTextDatum(name); \
+ INSTR_TIME_SET_CURRENT_FAST(t0); \
+ for(int64 i = 0; i < n; ++i) \
+ {
+
+#define END_TIMING \
+ } \
+ INSTR_TIME_SET_CURRENT_FAST(t1); \
+ INSTR_TIME_SET_ZERO(dt); \
+ INSTR_TIME_ACCUM_DIFF(dt, t1, t0); \
+ values[1] = Float8GetDatum((double)INSTR_TIME_GET_NANOSEC(dt) / (double) (n)); \
+ values[2] = Int64GetDatum(n); \
+ values[3] = Int64GetDatum(++timing_operation_id); \
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); \
+}
+
+/* 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)
+
+
+/*
+ * 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;
+ int32 *r_ord,
+ *w_ord;
+ BufferTag *ptag,
+ *atag;
+ uint64 *phash,
+ *ahash;
+ int64 nfree = 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};
+
+ 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(uint64) * n);
+ ahash = palloc(sizeof(uint64) * 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).
+ */
+ r_ord = palloc(sizeof(int64) * n);
+ w_ord = palloc(sizeof(int64) * n);
+ for (int64 i = 0; i < n; i++){
+ r_ord[i] = i;
+ w_ord[i] = i;
+ }
+ if (randomize)
+ {
+ pg_prng_state rng;
+ pg_prng_seed(&rng, 0x9E3779B97F4A7C15ULL);
+
+ for (int64 i = n - 1; i > 0; i--)
+ {
+ int32 k;
+ int32 tmp;
+ k = pg_prng_int64_range(&rng, 0, i);
+ tmp = r_ord[i]; r_ord[i] = r_ord[k]; r_ord[k] = tmp;
+
+ k = pg_prng_int64_range(&rng, 0, i);
+ tmp = w_ord[i]; w_ord[i] = w_ord[k]; w_ord[k] = tmp;
+
+ k = pg_prng_int64_range(&rng, 0, i);
+ tmp = bufids[i]; bufids[i] = bufids[k]; bufids[k] = tmp;
+ }
+
+ }
+
+ if (!timing_initialized)
+ pg_initialize_timing();
+
+ PG_TRY();
+ {
+ for (int64 r = 0; r < rounds; r++)
+ {
+ timing_operation_id = 0;
+ BEGIN_TIMING("insert", n)
+ int32 j = w_ord[i];
+ BufTableInsert(&ptag[j], phash[j], bufids[j]);
+ END_TIMING
+
+ BEGIN_TIMING("hit", n)
+ int32 j = r_ord[i];
+ sink += BufTableLookup(&ptag[j], phash[j]);
+ END_TIMING;
+
+ BEGIN_TIMING("miss", n)
+ int32 j = r_ord[i];
+ sink += BufTableLookup(&atag[j], ahash[j]);
+ END_TIMING
+
+ BEGIN_TIMING("del", n)
+ int32 j = r_ord[i];
+ BufTableDelete(&ptag[j], phash[j]);
+ END_TIMING
+
+ BEGIN_TIMING("hash", n)
+ int32 j = r_ord[i];
+ sink += BufTableHashCode(&ptag[j]);
+ END_TIMING
+
+ BEGIN_TIMING("compare", n)
+ int32 j = r_ord[i];
+ sink += ext_BufferTagsEqual(&ptag[j], &ptag[i]);
+ END_TIMING
+
+ BEGIN_TIMING("nop", n)
+ int32 j = r_ord[i];
+ ext_nop(&ptag[j], phash[j]);
+ END_TIMING
+ }
+ }
+ 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();
+ (void) sink;
+ return (Datum) 0;
+}
\ No newline at end of file
diff --git a/src/test/modules/buftable_bench/buftable_bench.control b/src/test/modules/buftable_bench/buftable_bench.control
new file mode 100644
index 00000000000..aae6cb1810f
--- /dev/null
+++ b/src/test/modules/buftable_bench/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/src/test/modules/buftable_bench/meson.build b/src/test/modules/buftable_bench/meson.build
new file mode 100644
index 00000000000..070d79e96b8
--- /dev/null
+++ b/src/test/modules/buftable_bench/meson.build
@@ -0,0 +1,23 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+buftable_bench_sources = files(
+ 'buftable_bench.c',
+ 'no_optimise.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/src/test/modules/buftable_bench/no_optimise.c b/src/test/modules/buftable_bench/no_optimise.c
new file mode 100644
index 00000000000..2fffcd471e8
--- /dev/null
+++ b/src/test/modules/buftable_bench/no_optimise.c
@@ -0,0 +1,27 @@
+/*-------------------------------------------------------------------------
+ *
+ * no_optimise.c
+ * Opaque call targets for the buftable_bench dummy ops.
+ *
+ * These live in a separate translation unit so the compiler compiling
+ * buftable_bench.c cannot see the bodies, inline them, or delete unused
+ * calls. (Does not survive -flto.)
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "no_optimise.h"
+
+void
+ext_nop(BufferTag *tag, uint64 hashcode)
+{
+ (void) tag;
+ (void) hashcode;
+}
+
+bool
+ext_BufferTagsEqual(const BufferTag *tag1, const BufferTag *tag2)
+{
+ return BufferTagsEqual(tag1, tag2);
+}
diff --git a/src/test/modules/buftable_bench/no_optimise.h b/src/test/modules/buftable_bench/no_optimise.h
new file mode 100644
index 00000000000..83f34e353bc
--- /dev/null
+++ b/src/test/modules/buftable_bench/no_optimise.h
@@ -0,0 +1,16 @@
+/*-------------------------------------------------------------------------
+ *
+ * no_optimise.h
+ * Declarations for opaque dummy ops (bodies in no_optimise.c).
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NO_OPTIMISE_H
+#define NO_OPTIMISE_H
+
+#include "storage/buf_internals.h"
+
+extern void ext_nop(BufferTag *tag, uint64 hashcode);
+extern bool ext_BufferTagsEqual(const BufferTag *tag1, const BufferTag *tag2);
+
+#endif /* NO_OPTIMISE_H */
--
2.53.0
From 62fd0eea23eab846bed6801017d4ca4a33094b70 Mon Sep 17 00:00:00 2001
From: Dhruv Aron <[email protected]>
Date: Wed, 8 Jul 2026 17:29:00 +0100
Subject: [PATCH-v1 1/2] Inline SharedBufHash
Replaces the dynamic hash shared buffer lookup table with an inline
implementation of a separate chaining hash array implementation,
with pinned memory.
buckets[num_buckets] - hash lookup
entries[NBuffers] - linked-list<BufferTag>
buckets are int32 (4 bytes), and entries is a linked list node
containing a buffer tag and a pointer to the next item in the chain
The dynhash previously used required about 56 bytes per entry.
On a 64-bit machine
elementSize = MAXALIGN(sizeof(HASHELEMENT)) + MAXALIGN(hctl->entrysize);
With .hash_info.keysize = sizeof(BufferTag) = 20
it was MAXALIGN(12) + MAXALIGN(20) = 16 + 24 = 40
assuming buckets = entries we would have another 8 bytes per bucket head
and 8 bytes per segment, assuming buckets = 2 * entries we would have
stounf 56 + 2*8/256, not too far from allocated size.
The execution flow, in hash_search_with_hash_value had,
in addition to the external call, at least 6 extra branches,
and about the same number of additional pointer dereferences
Memory per bucket reduced from 8 to 4
Memory per entry reduced from 40 to 24
The entries live in a single array about 2k times smaller
than shared_buffers, e.g. 1GB of shared buffers mean 3MB
entries. And about 1MB for buckets (and occupation < 50%).
---
src/backend/storage/buffer/buf_table.c | 224 ++++++++++++++++++-------
1 file changed, 168 insertions(+), 56 deletions(-)
diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c
index 347bf267d73..7836d0dd664 100644
--- a/src/backend/storage/buffer/buf_table.c
+++ b/src/backend/storage/buffer/buf_table.c
@@ -3,6 +3,27 @@
* buf_table.c
* routines for mapping BufferTags to buffer indexes.
*
+ * The shared buffer mapping table is a flat, index-linked hash table (an
+ * open-chaining replacement for the former dynahash-based table). It is made
+ * of two shared-memory arrays:
+ *
+ * buckets[num_buckets] - one chain head per hash bucket
+ * entries[NBuffers] - one entry per buffer, indexed by buf_id
+ *
+ * Each buffer slot i permanently owns entry slot i, so no freelist is needed:
+ * bufmgr always removes a buffer's old mapping (BufTableDelete, called from
+ * InvalidateVictimBuffer) before inserting a new tag for that same buf_id (see
+ * GetVictimBuffer / BufferAlloc in bufmgr.c). Empty entry slots are marked by
+ * tag.blockNum == P_NEW; chains are linked by int index and terminated by
+ * BUF_TABLE_CHAIN_END.
+ *
+ * num_buckets is a power of two and a multiple of NUM_BUFFER_PARTITIONS, so the
+ * bucket index (hashcode % num_buckets) shares its low bits with the partition
+ * index (hashcode % NUM_BUFFER_PARTITIONS). Every tag that maps to a given
+ * bucket therefore maps to a single partition, and the caller's BufMappingLock
+ * fully serializes each chain -- the same guarantee the dynahash table relied
+ * on.
+ *
* Note: the routines in this file do no locking of their own. The caller
* must hold a suitable lock on the appropriate BufMappingLock, as specified
* in the comments. We can't do the locking inside these functions because
@@ -21,56 +42,114 @@
*/
#include "postgres.h"
+#include "common/hashfn.h"
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
#include "storage/buf_internals.h"
+#include "storage/bufmgr.h"
+#include "storage/shmem.h"
#include "storage/subsystems.h"
+#define BUF_TABLE_CHAIN_END (-1)
+
+/* bucket for buffer lookup hashtable */
+typedef struct
+{
+ int head; /* head of hash chain, or BUF_TABLE_CHAIN_END */
+} BufferLookupBucket;
+
/* entry for buffer lookup hashtable */
typedef struct
{
- BufferTag key; /* Tag of a disk page */
- int id; /* Associated buffer ID */
+ BufferTag tag; /* Tag of a disk page, or P_NEW if empty */
+ int next; /* next entry in hash chain */
} BufferLookupEnt;
-static HTAB *SharedBufHash;
+/* bucket and entry arrays for buffer lookup hashtable (in shared memory) */
+static BufferLookupBucket *buckets;
+static BufferLookupEnt *entries;
+
+/* number of hash buckets; power of two and multiple of NUM_BUFFER_PARTITIONS */
+static int num_buckets;
static void BufTableShmemRequest(void *arg);
+static void BufTableShmemInit(void *arg);
+static void BufTableShmemAttach(void *arg);
const ShmemCallbacks BufTableShmemCallbacks = {
.request_fn = BufTableShmemRequest,
- /* no special initialization needed, the hash table will start empty */
+ .init_fn = BufTableShmemInit,
+ .attach_fn = BufTableShmemAttach,
};
/*
- * Register shmem hash table for mapping buffers.
- * size is the desired hash table size (possibly more than NBuffers)
+ * Number of hash buckets for the current NBuffers.
+ *
+ * Must be a power of two (so hashcode % num_buckets == hashcode & (num_buckets
+ * - 1)) and a multiple of NUM_BUFFER_PARTITIONS, so that every tag in a bucket
+ * maps to a single buffer partition (see file header).
+ */
+static inline int
+BufTableNumBuckets(void)
+{
+ return Max(NUM_BUFFER_PARTITIONS, pg_nextpower2_32(1.5 * NBuffers));
+}
+
+/*
+ * Register shared memory arrays for mapping buffers.
*/
void
BufTableShmemRequest(void *arg)
{
- int size;
+ num_buckets = BufTableNumBuckets();
+ Assert(num_buckets % NUM_BUFFER_PARTITIONS == 0);
- /*
- * Request the shared buffer lookup hashtable.
- *
- * Since we can't tolerate running out of lookup table entries, we must be
- * sure to specify an adequate table size here. The maximum steady-state
- * usage is of course NBuffers entries, but BufferAlloc() tries to insert
- * a new entry before deleting the old. In principle this could be
- * happening in each partition concurrently, so we could need as many as
- * NBuffers + NUM_BUFFER_PARTITIONS entries.
- */
- size = NBuffers + NUM_BUFFER_PARTITIONS;
-
- ShmemRequestHash(.name = "Shared Buffer Lookup Table",
- .nelems = size,
- .ptr = &SharedBufHash,
- .hash_info.keysize = sizeof(BufferTag),
- .hash_info.entrysize = sizeof(BufferLookupEnt),
- .hash_info.num_partitions = NUM_BUFFER_PARTITIONS,
- .hash_flags = HASH_ELEM | HASH_BLOBS | HASH_PARTITION | HASH_FIXED_SIZE,
+ ShmemRequestStruct(.name = "Shared Buffer Lookup Buckets",
+ .size = (Size) num_buckets * sizeof(BufferLookupBucket),
+ .ptr = (void **) &buckets,
+ );
+
+ ShmemRequestStruct(.name = "Shared Buffer Lookup Entries",
+ .size = (Size) NBuffers * sizeof(BufferLookupEnt),
+ .ptr = (void **) &entries,
);
}
+/*
+ * Initialize the shared buffer lookup table. Called once during shared-memory
+ * initialization (in the postmaster, or in a standalone backend).
+ *
+ * Shared memory is zeroed, but zero is a valid buf_id and block 0 is a valid
+ * block number, so we must explicitly mark every bucket empty
+ * (BUF_TABLE_CHAIN_END) and every entry empty (tag.blockNum == P_NEW).
+ */
+void
+BufTableShmemInit(void *arg)
+{
+ num_buckets = BufTableNumBuckets();
+
+ for (int i = 0; i < num_buckets; i++)
+ buckets[i].head = BUF_TABLE_CHAIN_END;
+
+ for (int i = 0; i < NBuffers; i++)
+ {
+ entries[i].tag.blockNum = P_NEW;
+ entries[i].next = BUF_TABLE_CHAIN_END;
+ }
+}
+
+/*
+ * Per-backend attach. The buckets/entries pointers are restored by the shmem
+ * framework, but num_buckets is a process-local scalar that must be recomputed
+ * in each backend. Forked children inherit it, but EXEC_BACKEND children run
+ * only the attach callback, so set it here too.
+ */
+void
+BufTableShmemAttach(void *arg)
+{
+ num_buckets = BufTableNumBuckets();
+}
+
/*
* BufTableHashCode
* Compute the hash code associated with a BufferTag
@@ -83,7 +162,7 @@ BufTableShmemRequest(void *arg)
uint32
BufTableHashCode(BufferTag *tagPtr)
{
- return get_hash_value(SharedBufHash, tagPtr);
+ return tag_hash(tagPtr, sizeof(BufferTag));
}
/*
@@ -95,19 +174,15 @@ BufTableHashCode(BufferTag *tagPtr)
int
BufTableLookup(BufferTag *tagPtr, uint32 hashcode)
{
- BufferLookupEnt *result;
-
- result = (BufferLookupEnt *)
- hash_search_with_hash_value(SharedBufHash,
- tagPtr,
- hashcode,
- HASH_FIND,
- NULL);
-
- if (!result)
- return -1;
+ int id = buckets[hashcode % num_buckets].head;
- return result->id;
+ while (id != BUF_TABLE_CHAIN_END)
+ {
+ if (BufferTagsEqual(&entries[id].tag, tagPtr))
+ return id;
+ id = entries[id].next;
+ }
+ return -1;
}
/*
@@ -123,23 +198,35 @@ BufTableLookup(BufferTag *tagPtr, uint32 hashcode)
int
BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id)
{
- BufferLookupEnt *result;
- bool found;
+ int bucket_id = hashcode % num_buckets;
+ int head = buckets[bucket_id].head;
+ int id = head;
- Assert(buf_id >= 0); /* -1 is reserved for not-in-table */
+ Assert(buf_id >= 0 && buf_id < NBuffers);
Assert(tagPtr->blockNum != P_NEW); /* invalid tag */
- result = (BufferLookupEnt *)
- hash_search_with_hash_value(SharedBufHash,
- tagPtr,
- hashcode,
- HASH_ENTER,
- &found);
+ /* If the tag is already in the chain, surface the existing buf_id. */
+ while (id != BUF_TABLE_CHAIN_END)
+ {
+ if (BufferTagsEqual(&entries[id].tag, tagPtr))
+ return id;
+ id = entries[id].next;
+ }
- if (found) /* found something already in the table */
- return result->id;
+ /*
+ * Not present. entry[buf_id] must be empty: bufmgr always deletes a
+ * buffer's old mapping before inserting a new tag for that buf_id.
+ */
+ Assert(entries[buf_id].tag.blockNum == P_NEW);
- result->id = buf_id;
+ /*
+ * Link entry[buf_id] at the chain head, keeping the prior head as its
+ * successor. (Use the saved `head`, not `id`, which the loop above has
+ * advanced to BUF_TABLE_CHAIN_END.)
+ */
+ entries[buf_id].tag = *tagPtr;
+ entries[buf_id].next = head;
+ buckets[bucket_id].head = buf_id;
return -1;
}
@@ -153,15 +240,32 @@ BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id)
void
BufTableDelete(BufferTag *tagPtr, uint32 hashcode)
{
- BufferLookupEnt *result;
+ int bucket_id = hashcode % num_buckets;
+ int prev = BUF_TABLE_CHAIN_END;
+ int id = buckets[bucket_id].head;
- result = (BufferLookupEnt *)
- hash_search_with_hash_value(SharedBufHash,
- tagPtr,
- hashcode,
- HASH_REMOVE,
- NULL);
+ while (id != BUF_TABLE_CHAIN_END)
+ {
+ if (BufferTagsEqual(&entries[id].tag, tagPtr))
+ {
+ /* unlink from the chain */
+ if (prev == BUF_TABLE_CHAIN_END)
+ buckets[bucket_id].head = entries[id].next;
+ else
+ entries[prev].next = entries[id].next;
+ /* mark the entry empty */
+ entries[id].tag.blockNum = P_NEW;
+ entries[id].next = BUF_TABLE_CHAIN_END;
+ return;
+ }
+ prev = id;
+ id = entries[id].next;
+ }
- if (!result) /* shouldn't happen */
- elog(ERROR, "shared buffer hash table corrupted");
+ /*
+ * Entry not in table. Callers never double-delete (deletion is gated by
+ * BM_TAG_VALID on the buffer header), so this indicates corruption.
+ */
+ Assert(false);
+ elog(ERROR, "shared buffer hash table corrupted");
}
--
2.53.0