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
v2-0001-Buffer-table-test-module.patch
Description: Binary data
v2-0002-Inline-SharedBufHash.patch
Description: Binary data
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 "================================================================"
