From 7e2fe005525d59c4def5ac0f1060d3d4bfa43351 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Mon, 17 Aug 2026 18:19:43 +0100 Subject: [PATCH 1/5] Benchmark This adds a module for benchmarking LWLocks in a tight loop. This time I am structuring the benchmark module as a generic one that could be extended in the future. The idea is that under the module microbench a contributor that trying to do a low level benchmark could write a new component there and easily get pooled measurements exposed via SQL. microbench-head-1.0.sql implements a standard processing helping to understand not only the average performance but also rank statistics. e.g. op | avg | min | [q1 | med | q3] | max | std -------------+-------+------+------+------+------+-------+------ spin-lock | 10.07 | 9.11 | 9.77 | 9.77 | 9.77 | 19.21 | 1.20 LWLock-ex | 8.26 | 7.16 | 7.81 | 8.13 | 8.14 | 17.25 | 0.74 LWLock-sh | 8.18 | 7.16 | 7.81 | 7.81 | 8.14 | 10.74 | 0.72 LWLock-cond | 6.81 | 6.18 | 6.84 | 6.84 | 6.84 | 34.18 | 0.95 nop | 1.00 | 0.32 | 0.98 | 0.98 | 0.98 | 1.95 | 0.11 (5 rows) In this particular exmaple, spin-lock is the LockBufHdr and UnlockBufHdr, LWLock-ex and LWLock-sh represents LWLockAcquire followed by LWLockRelease, in exclusive and shared mode, respectively. LWLock-cond is a conditional shared lock followed by a LWLockRelease. nop is just reading the lwlock pointer in the loop and gives a baseline. Where statistics of average times of each measurement are computed as follows: avg: average time per min: above 0% of the data (obviously) q1: end of first quartile, above 25% med: short for median, above 50% q2: end of third quartile, above 75% max: above 100% of the data (obviously) std: standard deviation, measures how dispersed is the data. To reduce the impact of the time measurement itself one can repeat the operation a large number of times in a loop between two timer observations. One looses the individual measurements but the averages are still going to reflect to some extent the variability of the operation itself. --- src/test/modules/microbench/Makefile | 61 ++++++++ src/test/modules/microbench/lwlock/bench.c | 123 ++++++++++++++++ .../modules/microbench/lwlock/install.sql | 10 ++ src/test/modules/microbench/lwlock/query.sql | 8 ++ src/test/modules/microbench/meson.build | 33 +++++ .../microbench/microbench-head--1.0.sql | 44 ++++++ .../modules/microbench/microbench.control | 4 + src/test/modules/microbench/randomize.h | 23 +++ .../modules/microbench/scripts/run-test.sh | 135 ++++++++++++++++++ src/test/modules/microbench/timing-magic.h | 30 ++++ 10 files changed, 471 insertions(+) create mode 100644 src/test/modules/microbench/Makefile create mode 100644 src/test/modules/microbench/lwlock/bench.c create mode 100644 src/test/modules/microbench/lwlock/install.sql create mode 100644 src/test/modules/microbench/lwlock/query.sql create mode 100644 src/test/modules/microbench/meson.build create mode 100644 src/test/modules/microbench/microbench-head--1.0.sql create mode 100644 src/test/modules/microbench/microbench.control create mode 100644 src/test/modules/microbench/randomize.h create mode 100755 src/test/modules/microbench/scripts/run-test.sh create mode 100644 src/test/modules/microbench/timing-magic.h diff --git a/src/test/modules/microbench/Makefile b/src/test/modules/microbench/Makefile new file mode 100644 index 00000000000..4cdbe4ff0c7 --- /dev/null +++ b/src/test/modules/microbench/Makefile @@ -0,0 +1,61 @@ +# src/test/modules/microbench/Makefile + +PGFILEDESC = "microbench - suite of functions for micro-benchmarks" + +EXTENSION = microbench +MODULE_big = microbench + +# One subdir per benchmark: /bench.c, /install.sql, /query.sql +MICROBENCH_TESTS := $(sort $(patsubst %/bench.c,%,$(wildcard */bench.c))) +SQL_FRAGMENTS := $(addsuffix /install.sql,$(MICROBENCH_TESTS)) + +MICROBENCH_SQL_HEAD = microbench-head--1.0.sql +MICROBENCH_SQL_BUILT = microbench--1.0.sql + +OBJS = \ + $(WIN32RES) \ + $(addsuffix /bench.o,$(MICROBENCH_TESTS)) + +# Generated extension script; must be DATA_built so all/install depend on it. +DATA_built = $(MICROBENCH_SQL_BUILT) + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/microbench +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk + +# Link the module against an explicit postgres binary when requested, e.g. +# make install MICROBENCH_POSTGRES=/path/to/bin/postgres +ifdef MICROBENCH_POSTGRES +BE_DLLLIBS := -bundle_loader $(MICROBENCH_POSTGRES) +endif +endif + +$(MICROBENCH_SQL_BUILT): $(addprefix $(srcdir)/,$(MICROBENCH_SQL_HEAD)) \ + $(addprefix $(srcdir)/,$(SQL_FRAGMENTS)) + cat $^ > $@ + +# Incremental compile of a single benchmark folder, e.g. make lwlock +$(MICROBENCH_TESTS): %: %/bench.o + +%/bench.o: %/bench.c randomize.h timing-magic.h + $(COMPILE.c) -I. -o $@ $< + +.PHONY: run tests list $(MICROBENCH_TESTS) + +tests: $(MICROBENCH_TESTS) + +list: + @echo $(MICROBENCH_TESTS) + +run: +ifndef TEST + $(error TEST is required, e.g. make run TEST=lwlock) +endif + @test -d '$(TEST)' || (echo "unknown test: $(TEST)" && exit 1) + $(SHELL) '$(srcdir)/scripts/run-test.sh' '$(TEST)' diff --git a/src/test/modules/microbench/lwlock/bench.c b/src/test/modules/microbench/lwlock/bench.c new file mode 100644 index 00000000000..33d75770a0d --- /dev/null +++ b/src/test/modules/microbench/lwlock/bench.c @@ -0,0 +1,123 @@ +/*------------------------------------------------------------------------- + * + * lwlock/bench.c + * Micro-benchmark of LWLock acquire/release paths. + * + * IDENTIFICATION + * src/test/modules/microbench/lwlock/bench.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "fmgr.h" +#include "funcapi.h" +#include "portability/instr_time.h" +#include "storage/buf_internals.h" +#include "storage/lwlock.h" +#include "utils/builtins.h" +#include "utils/tuplestore.h" + +#include "randomize.h" +#include "timing-magic.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(bench_lwlock); + +/* + * bench_lwlock(n, rounds, randomize) -> SETOF + * (op text, avg_ns float8, batch_size int8, id int8) + */ +Datum +bench_lwlock(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; + LWLock **locks; + volatile int64 sink PG_USED_FOR_ASSERTS_ONLY = 0; + + if (n <= 0 || rounds <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("n and rounds must be positive"))); + + if (n > NUM_BUFFER_PARTITIONS) + n = NUM_BUFFER_PARTITIONS; + + InitMaterializedSRF(fcinfo, 0); + + locks = palloc(sizeof(LWLock *) * n); + if (randomize) + { + pg_prng_state rng; + + pg_prng_seed(&rng, 0xDA7ABA5E); + for (int i = 0; i < n; i++) + locks[i] = BufMappingPartitionLock(i); + shuffle_pointers(&rng, (void **) locks, (int) n); + } + else + { + for (int i = 0; i < n; i++) + locks[i] = BufMappingPartitionLock(0); + } + + if (!timing_initialized) + pg_initialize_timing(); + + for (int64 r = 0; r < rounds; r++) + { + INIT_TIMING_SCOPE(); + { + BufferDesc *buf_desc = GetBufferDescriptor(1); + BEGIN_TIMING("spin-lock", n) + { + LockBufHdr(buf_desc); + UnlockBufHdr(buf_desc); + } + END_TIMING; + } + + BEGIN_TIMING("LWLock-ex", n) + { + LWLock *lock = locks[i]; + + LWLockAcquire(lock, LW_EXCLUSIVE); + LWLockRelease(lock); + } + END_TIMING; + + BEGIN_TIMING("LWLock-sh", n) + { + LWLock *lock = locks[i]; + + LWLockAcquire(lock, LW_SHARED); + LWLockRelease(lock); + } + END_TIMING; + + BEGIN_TIMING("LWLock-cond", n) + { + LWLock *lock = locks[i]; + + if (!LWLockConditionalAcquire(lock, LW_SHARED)) + elog(ERROR, "Failed to acquire lock"); + LWLockRelease(lock); + } + END_TIMING; + + BEGIN_TIMING("nop", n) + { + LWLock *lock = locks[i]; + + sink += (int64) (uintptr_t) lock; + } + END_TIMING; + } + + (void) sink; + return (Datum) 0; +} diff --git a/src/test/modules/microbench/lwlock/install.sql b/src/test/modules/microbench/lwlock/install.sql new file mode 100644 index 00000000000..9bf234837b1 --- /dev/null +++ b/src/test/modules/microbench/lwlock/install.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION bench_lwlock( + IN n int8, + IN rounds int8 DEFAULT 1, + IN random bool DEFAULT true +) +RETURNS SETOF microbench_sample +AS 'MODULE_PATHNAME', 'bench_lwlock' +LANGUAGE C; + +REVOKE ALL ON FUNCTION bench_lwlock(int8, int8, bool) FROM PUBLIC; diff --git a/src/test/modules/microbench/lwlock/query.sql b/src/test/modules/microbench/lwlock/query.sql new file mode 100644 index 00000000000..16c1fd1dfe4 --- /dev/null +++ b/src/test/modules/microbench/lwlock/query.sql @@ -0,0 +1,8 @@ +-- lwlock micro-benchmark query + +\pset format aligned + +SELECT * FROM format_microbench( + (SELECT array_agg(s ORDER BY s.id) + FROM bench_lwlock(:'n'::int8, :'rounds'::int8, true) AS s) +); diff --git a/src/test/modules/microbench/meson.build b/src/test/modules/microbench/meson.build new file mode 100644 index 00000000000..9836640a3df --- /dev/null +++ b/src/test/modules/microbench/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +microbench_tests = [ + 'lwlock', +] + +microbench_sources = files( + '@0@/bench.c'.format(test) for test in microbench_tests +) + +if host_system == 'windows' + microbench_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'microbench', + '--FILEDESC', 'microbench - micro-benchmark suite',]) +endif + +microbench_sql = custom_target('microbench--1.0.sql', + output: 'microbench--1.0.sql', + input: files('microbench--1.0.sql.head') + files( + '@0@/install.sql'.format(test) for test in microbench_tests + ), + command: [find_program('cat'), '@INPUT@'], + build_by_default: true, +) + +microbench = shared_module('microbench', + microbench_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += microbench + +test_install_data += microbench_sql +test_install_data += files('microbench.control') diff --git a/src/test/modules/microbench/microbench-head--1.0.sql b/src/test/modules/microbench/microbench-head--1.0.sql new file mode 100644 index 00000000000..48c5143843a --- /dev/null +++ b/src/test/modules/microbench/microbench-head--1.0.sql @@ -0,0 +1,44 @@ +/* src/test/modules/microbench/microbench--1.0.sql */ +/* Generated from microbench--1.0.sql.head and per-test install.sql files. */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION microbench" to load this file. \quit + +CREATE DOMAIN microbench_format AS numeric(15, 2); + +CREATE TYPE microbench_sample AS ( + op text, + avg_ns float8, + batch_size int8, + id int8 +); + +CREATE TYPE microbench_stats AS ( + op text, + avg microbench_format, + min microbench_format, + q1 microbench_format, + med microbench_format, + q3 microbench_format, + max microbench_format, + std microbench_format +); + +CREATE FUNCTION format_microbench(samples microbench_sample[]) +RETURNS SETOF microbench_stats +LANGUAGE sql +STABLE +AS $$ + SELECT + s.op, + avg(s.avg_ns), + min(s.avg_ns), + percentile_cont(0.25) WITHIN GROUP (ORDER BY s.avg_ns), + percentile_cont(0.50) WITHIN GROUP (ORDER BY s.avg_ns), + percentile_cont(0.75) WITHIN GROUP (ORDER BY s.avg_ns), + max(s.avg_ns), + stddev(s.avg_ns) + FROM unnest(samples) AS s + GROUP BY s.op + ORDER BY min(s.id); +$$; diff --git a/src/test/modules/microbench/microbench.control b/src/test/modules/microbench/microbench.control new file mode 100644 index 00000000000..16aaf002517 --- /dev/null +++ b/src/test/modules/microbench/microbench.control @@ -0,0 +1,4 @@ +comment = 'micro-benchmarks for LWLock acquire/release paths' +default_version = '1.0' +module_pathname = '$libdir/microbench' +relocatable = true diff --git a/src/test/modules/microbench/randomize.h b/src/test/modules/microbench/randomize.h new file mode 100644 index 00000000000..e6eb3e32542 --- /dev/null +++ b/src/test/modules/microbench/randomize.h @@ -0,0 +1,23 @@ +#ifndef MICROBENCH_RANDOMIZE_H +#define MICROBENCH_RANDOMIZE_H + +#include "common/pg_prng.h" + +/* + * Fisher-Yates shuffle of a pointer array. + * https://en.wikipedia.org/wiki/Fisher-Yates_shuffle + */ +static inline void +shuffle_pointers(pg_prng_state *rng, void **ptrs, int count) +{ + for (int i = count - 1; i > 0; i--) + { + int k = (int) pg_prng_int64_range(rng, 0, i); + void *tmp = ptrs[i]; + + ptrs[i] = ptrs[k]; + ptrs[k] = tmp; + } +} + +#endif diff --git a/src/test/modules/microbench/scripts/run-test.sh b/src/test/modules/microbench/scripts/run-test.sh new file mode 100755 index 00000000000..ee9e29d7b49 --- /dev/null +++ b/src/test/modules/microbench/scripts/run-test.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# +# Run one microbench test folder. +# +# Usage: run-test.sh TEST +# Env: TOP_BUILDDIR, PG_CONFIG, MICROBENCH_PORT (default 55432), +# MICROBENCH_N (default 128), MICROBENCH_ROUNDS (default 1000) +# +set -euo pipefail + +TEST=${1:?usage: run-test.sh TEST} + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +MODULE_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +TOP_BUILDDIR=${TOP_BUILDDIR:-$(cd "$MODULE_DIR/../../../.." && pwd)} +PORT=${MICROBENCH_PORT:-55432} +LOGDIR="$MODULE_DIR/.tmp_check/log" +DATADIR="$MODULE_DIR/.tmp_check/data" +N=${MICROBENCH_N:-128} +ROUNDS=${MICROBENCH_ROUNDS:-1000} + +log() { printf '%s\n' "$*" >&2; } + +test -f "$MODULE_DIR/$TEST/query.sql" || { + log "missing $MODULE_DIR/$TEST/query.sql" + exit 1 +} +pick_pg_config() { + if [[ -n "${PG_CONFIG:-}" && -x "$PG_CONFIG" ]]; then + printf '%s\n' "$PG_CONFIG" + return + fi + + local makefile_global="$TOP_BUILDDIR/src/Makefile.global" + if [[ -f "$makefile_global" ]]; then + local prefix candidate + + prefix=$(sed -n 's/^prefix := //p' "$makefile_global" | head -1) + if [[ -n "$prefix" ]]; then + candidate="$prefix/bin/pg_config" + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return + fi + log "configured prefix $prefix has no executable pg_config at $candidate" + fi + fi + + return 1 +} + +PG_CONFIG=$(pick_pg_config) || { + log "no PostgreSQL install found; set PG_CONFIG or run configure && make install at repo root" + exit 1 +} + +BINDIR=$("$PG_CONFIG" --bindir) +LIBDIR=$("$PG_CONFIG" --libdir) +log "==> using $($PG_CONFIG --version) [$BINDIR]" + +log "==> building and installing microbench..." +make -C "$MODULE_DIR" MICROBENCH_POSTGRES="$BINDIR/postgres" install + +export PATH="$BINDIR:$PATH" +case "$(uname -s)" in + Darwin) export DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" ;; + *) export LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" ;; +esac + +mkdir -p "$LOGDIR" + +postgres_build_id() { + # Re-init when the installed postgres binary changes (catalog bumps, rebuilds). + printf '%s:%s' "$("$BINDIR/postgres" --version)" \ + "$(shasum -a 256 "$BINDIR/postgres" | awk '{print $1}')" +} + +ensure_datadir() { + local build_id stamp_file="$DATADIR/.microbench_build_id" + + build_id=$(postgres_build_id) + if [[ -f "$DATADIR/PG_VERSION" && -f "$stamp_file" && "$(cat "$stamp_file")" == "$build_id" ]]; then + return 0 + fi + + if [[ -f "$DATADIR/PG_VERSION" ]]; then + log "==> stale datadir (postgres rebuilt); re-initdb..." + else + log "==> initdb..." + fi + + rm -rf "$DATADIR" + "$BINDIR/initdb" -D "$DATADIR" --auth trust --no-sync --no-instructions -N \ + >"$LOGDIR/initdb.log" 2>&1 + printf '%s\n' "$build_id" > "$stamp_file" +} + +ensure_datadir + +cleanup() { + if "$BINDIR/pg_ctl" -D "$DATADIR" status >/dev/null 2>&1; then + "$BINDIR/pg_ctl" -D "$DATADIR" stop -m fast >>"$LOGDIR/pg_ctl.log" 2>&1 || true + fi +} +trap cleanup EXIT + +if ! "$BINDIR/pg_ctl" -D "$DATADIR" status >/dev/null 2>&1; then + log "==> starting postgres on port $PORT..." + if ! "$BINDIR/pg_ctl" -D "$DATADIR" -l "$LOGDIR/postgres.log" \ + -o "-p $PORT -F -h '' -c shared_buffers=128MB" start \ + >>"$LOGDIR/pg_ctl.log" 2>&1; then + if grep -q 'incompatible with server' "$LOGDIR/postgres.log"; then + log "==> postgres rejected datadir; re-initdb..." + rm -rf "$DATADIR" + ensure_datadir + "$BINDIR/pg_ctl" -D "$DATADIR" -l "$LOGDIR/postgres.log" \ + -o "-p $PORT -F -h '' -c shared_buffers=128MB" start \ + >>"$LOGDIR/pg_ctl.log" 2>&1 + else + log "pg_ctl start failed; see $LOGDIR/postgres.log and $LOGDIR/pg_ctl.log" + exit 1 + fi + fi +fi + +log "==> CREATE EXTENSION microbench" +: >"$LOGDIR/psql.log" +"$BINDIR/psql" -v ON_ERROR_STOP=1 -p "$PORT" -d postgres \ + -c "DROP EXTENSION IF EXISTS microbench CASCADE; CREATE EXTENSION microbench;" \ + >>"$LOGDIR/psql.log" 2>&1 + +log "==> running $TEST/query.sql (n=$N rounds=$ROUNDS)" +"$BINDIR/psql" -v ON_ERROR_STOP=1 -p "$PORT" -d postgres \ + -v n="$N" -v rounds="$ROUNDS" \ + -f "$MODULE_DIR/$TEST/query.sql" diff --git a/src/test/modules/microbench/timing-magic.h b/src/test/modules/microbench/timing-magic.h new file mode 100644 index 00000000000..54318841f64 --- /dev/null +++ b/src/test/modules/microbench/timing-magic.h @@ -0,0 +1,30 @@ +#ifndef MICROBENCH_TIMING_MAGIC_H +#define MICROBENCH_TIMING_MAGIC_H + +#include "portability/instr_time.h" + +#define INIT_TIMING_SCOPE() \ + int64 timing_operation_id = 0 + +#define BEGIN_TIMING(name, n) \ + do { \ + 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); \ + } while (0) + +#endif -- 2.53.0