On Tue, Jul 7, 2026 at 8:14 PM Heikki Linnakangas <[email protected]> wrote:

> > bufmgr.c also changed slightly to prevent a race condition.
>
> Hmm, we're now holding the buffer header lock much longer than before,
> in InvalidateBuffer(). It's a spinlock, it really should not be held for
> more than a few instructions. BufTableDelete() is very fast in the new

implementation, but still.


As Andres later clarified the problem is not the duration of the lock, is
the complexity of the safety risk of what we do under it.

Could we perhaps do some of
> BufTableDelete()'s work ahead of time, before we acquire the buffer
> header lock?


This is a better approach in my opinion, we can do the buffer search before
acquiring the spin-lock and just break the chain link under the spin-lock.
It is easier to prove the safety of the later operation, and hopefully get
an
agreement on the suitability of the change.

I am adding a patch on top of Dhruv's work with that change.

I also implemented the deletion by Buffer (del-buf), about 30% faster than
original deletion by BufferTag (del-tag).

I added the buffer header lock (spin-lock), partition lock (LWLock),
and BufferHashCode to the benchmark. To see when we are entering
the diminishing returns zone.

0003 - 2-step deletion (indistinguishable from 0002), del-buf faster than
del-tag.

    op     | avg  | min  | [q1  | median | q3]  | p99  | std
-----------+------+------+------+--------+------+------+------
 insert    | 4.45 | 4.30 | 4.36 |   4.37 | 4.41 | 5.56 | 0.28
 hit       | 5.96 | 5.81 | 5.86 |   5.88 | 5.93 | 6.82 | 0.27
 del-buf   | 4.95 | 4.86 | 4.88 |   4.90 | 4.94 | 5.55 | 0.17
 insert    | 4.46 | 4.39 | 4.41 |   4.42 | 4.45 | 4.86 | 0.10
 miss      | 5.96 | 5.83 | 5.88 |   5.90 | 5.96 | 6.52 | 0.15
 del-tag   | 7.32 | 7.14 | 7.18 |   7.20 | 7.28 | 8.49 | 0.61
 LWLock-ex | 7.88 | 7.78 | 7.78 |   7.79 | 7.88 | 8.70 | 0.23
 LWLock    | 7.88 | 7.77 | 7.78 |   7.79 | 7.87 | 8.58 | 0.20
 HdrLock   | 4.99 | 4.74 | 4.80 |   5.03 | 5.08 | 5.68 | 0.19
 hash      | 4.44 | 4.37 | 4.38 |   4.39 | 4.44 | 4.97 | 0.16
 compare   | 1.37 | 1.31 | 1.34 |   1.35 | 1.35 | 1.52 | 0.34
 nop       | 0.91 | 0.88 | 0.90 |   0.90 | 0.91 | 1.03 | 0.04

0002 - Inline hash (Dhruv's original patch)
    op     | avg  | min  | [q1  | median | q3]  | p99  | std
-----------+------+------+------+--------+------+------+------
 insert    | 5.32 | 4.87 | 5.13 |   5.33 | 5.45 | 6.00 | 0.30
 hit       | 7.15 | 6.53 | 6.92 |   7.16 | 7.32 | 7.77 | 0.30
 miss      | 7.59 | 6.96 | 7.32 |   7.61 | 7.76 | 8.19 | 0.52
 del-tag   | 8.59 | 7.85 | 8.30 |   8.60 | 8.78 | 9.32 | 0.50
 LWLock-ex | 8.43 | 7.78 | 8.15 |   8.46 | 8.63 | 9.17 | 0.31
 LWLock    | 8.44 | 7.78 | 8.15 |   8.47 | 8.64 | 9.12 | 0.32
 HdrLock   | 5.34 | 4.78 | 5.19 |   5.32 | 5.51 | 5.82 | 0.25
 hash      | 4.74 | 4.38 | 4.59 |   4.75 | 4.85 | 5.17 | 0.20
 compare   | 1.48 | 1.37 | 1.42 |   1.47 | 1.53 | 1.67 | 0.08
 nop       | 0.98 | 0.90 | 0.94 |   0.97 | 1.02 | 1.11 | 0.05

0001 - Benchmark (master)
    op     |  avg  |  min  |  [q1  | median |  q3]  |  p99  | std
-----------+-------+-------+-------+--------+-------+-------+------
 insert    | 16.28 | 15.48 | 15.65 |  15.75 | 16.18 | 23.91 | 2.61
 hit       | 13.31 | 12.51 | 12.72 |  12.76 | 12.99 | 17.45 | 7.70
 miss      | 13.29 | 12.65 | 12.94 |  12.98 | 13.20 | 16.34 | 2.21
 del-tag   | 10.31 |  9.66 |  9.96 |   9.99 | 10.15 | 15.33 | 1.72
 LWLock-ex |  7.94 |  7.59 |  7.79 |   7.80 |  7.93 |  9.17 | 0.63
 LWLock    |  7.92 |  7.65 |  7.78 |   7.79 |  7.91 | 10.00 | 0.46
 HdrLock   |  5.01 |  4.65 |  4.81 |   5.03 |  5.10 |  5.70 | 0.32
 hash      |  4.56 |  4.39 |  4.47 |   4.51 |  4.57 |  5.18 | 0.20
 compare   |  1.40 |  1.34 |  1.37 |   1.37 |  1.39 |  1.68 | 0.09
 nop       |  0.92 |  0.88 |  0.90 |   0.90 |  0.92 |  1.10 | 0.05


Regards,
Alexandre

Attachment: v2-0001-Buffer-table-test-module.patch
Description: Binary data

Attachment: v2-0002-Inline-SharedBufHash.patch
Description: Binary data

Attachment: v2-0003-Mutable-links.patch
Description: Binary data

#!/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)"
WORK="${BUFTABLE_BENCH_WORK:-$HERE/_work}"
mkdir -p "$WORK"
ROUNDS="${BUFTABLE_PROBE_ROUNDS:-10}"
SIZES="${*:-1GB}"
BUILD_LOG="$WORK/build.log"

# Progress on stderr so result tables on stdout stay parseable.
log() { printf '%s\n' "$*" >&2; }

repo="$(git -C "$HERE" rev-parse --show-toplevel)"
# stg is handy to edit commits (https://stacked-git.github.io/)
commit=""
if commit="$(cd "$repo" && stg series 2>/dev/null | awk '$1 == ">" { print $2; found=1 } END { exit !found }')"; then
	:
else
	commit="$(git -C "$repo" rev-parse --short HEAD)"
fi
branch="$(git -C "$repo" rev-parse --abbrev-ref HEAD)"
label="$branch@$commit"

# Linux installs buftable_bench.so; macOS installs buftable_bench.dylib.
buftable_bench_installed() {
	local pkglibdir="$("$1/bin/pg_config" --pkglibdir)"
	local ext
	for ext in so dylib dll; do
		if [[ -e "$pkglibdir/buftable_bench.$ext" ]]; then
			return 0
		fi
	done
	return 1
}

# ---- locate or build a release install of the current HEAD ------------------
if [[ -n "${PG_CONFIG:-}" ]]; then
	BIN="$("$PG_CONFIG" --bindir)"
	log "==> using PG_CONFIG build: $("$PG_CONFIG" --version) [$BIN]"
else
	prefix="$WORK/.builds/buftable"
	src="$WORK/src"
	log "==> building $label as release (log: $BUILD_LOG)..."
	if [[ -d "$src" ]]; then
		git -C "$src" fetch "$repo"
		git -C "$src" reset --hard "$(git -C "$repo" rev-parse HEAD)"
	else
		git -C "$repo" worktree add -f "$src" HEAD
		(
			cd "$src"
			./configure --prefix="$prefix" \
				--without-icu \
				--without-zlib \
				--without-readline
		) >"$BUILD_LOG" 2>&1 || {
			log "configure failed; see $BUILD_LOG"
			tail -n 40 "$BUILD_LOG" >&2
			exit 1
		}
	fi
	(
		cd "$src"
		# Install the server too.  `make -C …/buftable_bench install` only
		# copies the extension, leaving prefix/bin empty — initdb then fails
		# with "command not found" after a silent compile.
		make -j"$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)"
		make install
		make -C src/test/modules/buftable_bench install
	) >>"$BUILD_LOG" 2>&1 || {
		log "build failed; see $BUILD_LOG"
		tail -n 80 "$BUILD_LOG" >&2
		exit 1
	}
	log "==> built $label"
	BIN="$prefix/bin"
fi

if [[ ! -x "$BIN/initdb" || ! -x "$BIN/postgres" ]]; then
	log "error: no postgres install at $BIN (initdb/postgres missing)."
	log "The bench tree must be 'make install'd into the prefix, not only compiled."
	exit 1
fi
if ! buftable_bench_installed "$(dirname "$BIN")"; then
	log "error: buftable_bench extension is not installed under $BIN/.."
	exit 1
fi

# ---- run the probe per size -------------------------------------------------
size_to_bytes() {
	local s
	s="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')"
	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}')"
	LOG="$DATADIR/log"

	log "==> initdb + start shared_buffers=$SIZE (n=$N keys, rounds=$ROUNDS)"
	"$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
	if ! "$BIN/pg_ctl" -D "$DATADIR" -l "$LOG" -w start >/dev/null; then
		log "pg_ctl start failed; server log:"
		cat "$LOG" >&2 || true
		rm -rf "$DATADIR" "$SOCKDIR"
		exit 1
	fi

	num="numeric(15,2)"
	echo "-- shared_buffers=$SIZE  (n=$N keys) --"
	log "==> running probe (this is silent until psql returns)"
	if ! "$BIN/psql" -h "$SOCKDIR" -d postgres -q -X -P pager=off -v ON_ERROR_STOP=1 \
		-c "CREATE EXTENSION buftable_bench;" \
		-c "SELECT op,
		           avg(avg_ns)::$num AS avg,
		           min(avg_ns)::$num AS min,
				   percentile_disc(0.25) WITHIN GROUP (ORDER BY avg_ns)::$num AS \"[q1\",
				   percentile_disc(0.50) WITHIN GROUP (ORDER BY avg_ns)::$num AS median,
				   percentile_disc(0.75) WITHIN GROUP (ORDER BY avg_ns)::$num AS \"q3]\",
				   percentile_disc(0.99) WITHIN GROUP (ORDER BY avg_ns)::$num AS p99,
				   stddev(avg_ns)::$num AS std
			FROM buftable_bench_probe($N, $ROUNDS, false)
		    GROUP BY op, id
			ORDER BY id;"
	then
		log "psql probe failed; server log:"
		cat "$LOG" >&2 || true
		"$BIN/pg_ctl" -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true
		rm -rf "$DATADIR" "$SOCKDIR"
		exit 1
	fi

	"$BIN/pg_ctl" -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true
	rm -rf "$DATADIR" "$SOCKDIR"
done
echo "================================================================"

Reply via email to