From e065f412473b9bdba81cb01f56cbe6b629d78827 Mon Sep 17 00:00:00 2001 From: Alexandre Felipe Date: Mon, 14 Sep 2026 12:39:47 +0100 Subject: [PATCH-v4.0 1/5] Benchmark Add benchmark for buffer-mapping and LWLock under src/test/modules/microbench This capture low level data in C and exposes via SQL where one can use more powerful analysis easily. In my previous benchmark it was pointed out that a single process would inflate the L1 hit ratio, and in practice we want to see the effect of taking the shared variables from one core to another so this includes a multi-backend setup where multiple workers run the benchmark function. --- src/test/modules/microbench/Makefile | 62 ++++ src/test/modules/microbench/bufmap/bench.c | 226 +++++++++++++ .../modules/microbench/bufmap/install.sql | 10 + src/test/modules/microbench/bufmap/query.sql | 35 ++ src/test/modules/microbench/lwlock/bench.c | 135 ++++++++ .../modules/microbench/lwlock/install.sql | 10 + src/test/modules/microbench/lwlock/query.sql | 30 ++ src/test/modules/microbench/meson.build | 35 ++ .../modules/microbench/microbench--1.0.sql | 103 ++++++ .../microbench/microbench-head--1.0.sql | 83 +++++ .../modules/microbench/microbench.control | 4 + src/test/modules/microbench/multiprocessing.c | 312 ++++++++++++++++++ src/test/modules/microbench/multiprocessing.h | 30 ++ src/test/modules/microbench/randomize.h | 26 ++ .../modules/microbench/scripts/run-test.sh | 134 ++++++++ src/test/modules/microbench/timing-magic.h | 73 ++++ 16 files changed, 1308 insertions(+) create mode 100644 src/test/modules/microbench/Makefile create mode 100644 src/test/modules/microbench/bufmap/bench.c create mode 100644 src/test/modules/microbench/bufmap/install.sql create mode 100644 src/test/modules/microbench/bufmap/query.sql 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--1.0.sql 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/multiprocessing.c create mode 100644 src/test/modules/microbench/multiprocessing.h 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..4db6fce35e8 --- /dev/null +++ b/src/test/modules/microbench/Makefile @@ -0,0 +1,62 @@ +# 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) \ + multiprocessing.o \ + $(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 multiprocessing.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/bufmap/bench.c b/src/test/modules/microbench/bufmap/bench.c new file mode 100644 index 00000000000..d3f508b0879 --- /dev/null +++ b/src/test/modules/microbench/bufmap/bench.c @@ -0,0 +1,226 @@ +/*------------------------------------------------------------------------- + * + * bufmap/bench.c + * Micro-benchmark BufTable insert/lookup/delete with synthetic tags. + * No anchor table required - uses free buffer slots directly. + * + * IDENTIFICATION + * src/test/modules/microbench/bufmap/bench.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/table.h" +#include "catalog/namespace.h" +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "storage/buf_internals.h" +#include "storage/bufmgr.h" +#include "storage/lwlock.h" +#include "utils/builtins.h" +#include "utils/rel.h" +#include "utils/tuplestore.h" + +#include "multiprocessing.h" +#include "randomize.h" +#include "timing-magic.h" + +/* 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) + +PG_FUNCTION_INFO_V1(bench_bufmap); + +static void +run_bufmap_bench(int proc_id, int n_parallel, int rounds, int iterations, + ReturnSetInfo *rsinfo) +{ + intptr_t *blks; + BufferTag *ptags; + BufferTag *atags; + intptr_t *bufids; + pg_prng_state rng; + int nfree = 0; + LWLock *arbitrary_lock; + volatile int64 sink PG_USED_FOR_ASSERTS_ONLY = 0; + int start_buf; + 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}; + + blks = palloc(sizeof(intptr_t) * iterations); + ptags = palloc0(sizeof(BufferTag) * iterations); + atags = palloc0(sizeof(BufferTag) * iterations); + bufids = palloc(sizeof(intptr_t) * iterations); + pg_prng_seed(&rng, 0xB0FF0A00 ^ (uint64)proc_id); + + /* + * Each worker collects buffer IDs from a distinct range to avoid spinlock + * contention. Worker 1 starts at buffer 0, worker 2 at iterations, etc. + * We need n_parallel * iterations total free buffers. + */ + start_buf = (proc_id - 1) * iterations; + + elog(LOG, "Worker %d: starting buffer collection from %d", proc_id, start_buf); + + arbitrary_lock = BufMappingPartitionLock(0); + LWLockAcquire(arbitrary_lock, LW_EXCLUSIVE); + for (int i = start_buf; i < NBuffers && nfree < iterations; i++) + { + BufferDesc *desc = GetBufferDescriptor(i); + uint64 state = pg_atomic_read_u64(&desc->state); + + if (!(state & (BM_TAG_VALID | BM_LOCKED | BUF_REFCOUNT_MASK))) + { + state = LockBufHdr(desc); + UnlockBufHdrExt(desc, state, 0, 0, 1); + bufids[nfree++] = (intptr_t) i; + } + } + LWLockRelease(arbitrary_lock); + elog(LOG, "Worker %d: collected %d buffers", proc_id, nfree); + + if (nfree < iterations) + goto teardown; + + for (int i = 0; i < iterations; ++i) + { + int idx = n_parallel * i + proc_id - 1; + blks[i] = (intptr_t) i; + + InitBufferTag(&ptags[i], &rp, MAIN_FORKNUM, (BlockNumber) idx); + InitBufferTag(&atags[i], &ra, MAIN_FORKNUM, (BlockNumber) idx); + } + + if (!timing_initialized) + pg_initialize_timing(); + + elog(LOG, "Worker %d: setup complete, entering timing loops", proc_id); + + for (int64 r = 0; r < rounds; r++) + { + INIT_TIMING_SCOPE(); + shuffle_pointers(&rng, (void **) blks, iterations); + shuffle_pointers(&rng, (void **) bufids, iterations); + + BEGIN_GROUPED_TIMING("insert", iterations, 2) + { + BufferTag *tag = &ptags[blks[i]]; + uint32 hash; + LWLock *lock; + + hash = BufTableHashCode(tag); + lock = BufMappingPartitionLock(hash); + group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1; + sink += BufTableInsert(tag, hash, (Buffer)bufids[i]); + LWLockRelease(lock); + } + END_GROUPED_TIMING; + + BEGIN_GROUPED_TIMING("hit", iterations, 2) + { + BufferTag *tag = &ptags[blks[i]]; + uint32 hash; + LWLock *lock; + + hash = BufTableHashCode(tag); + lock = BufMappingPartitionLock(hash); + group_id = LWLockAcquire(lock, LW_SHARED) ? 0 : 1; + BufTableLookup(tag, hash); + LWLockRelease(lock); + } + END_GROUPED_TIMING; + + BEGIN_GROUPED_TIMING("miss", iterations, 2) + { + BufferTag *tag = &atags[blks[i]]; + uint32 hash; + LWLock *lock; + + hash = BufTableHashCode(tag); + lock = BufMappingPartitionLock(hash); + group_id = LWLockAcquire(lock, LW_SHARED) ? 0 : 1; + BufTableLookup(tag, hash); + LWLockRelease(lock); + } + END_GROUPED_TIMING; + + BEGIN_GROUPED_TIMING("delete", iterations, 2) + { + BufferTag *tag = &ptags[blks[i]]; + uint32 hash; + LWLock *lock; + + hash = BufTableHashCode(tag); + lock = BufMappingPartitionLock(hash); + group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1; + BufTableDelete(tag, hash); + LWLockRelease(lock); + } + END_GROUPED_TIMING; + + BEGIN_GROUPED_TIMING("LWLock", iterations, 2) + { + BufferTag *tag = &ptags[blks[i]]; + uint32 hash; + LWLock *lock; + + hash = BufTableHashCode(tag); + lock = BufMappingPartitionLock(hash); + group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1; + LWLockRelease(lock); + } + END_GROUPED_TIMING; + } + +teardown: + for (int i = 0; i < nfree; ++i){ + BufferDesc *desc = GetBufferDescriptor(bufids[i]); + uint64 state = LockBufHdr(desc); + UnlockBufHdrExt(desc, state, 0, 0, -1); + } + + pfree(bufids); + pfree(ptags); + pfree(atags); + pfree(blks); + (void) sink; + if(nfree < iterations) + elog(ERROR, "Couldn't get enough free buffers"); +} + +static void +microbench_parallel_work(int proc_id, int n_parallel, int rounds, int iterations) +{ + run_bufmap_bench(proc_id, n_parallel, rounds, iterations, NULL); +} + +/* + * bench_bufmap(n_parallel, rounds, iterations) -> SETOF microbench_sample + */ +Datum +bench_bufmap(PG_FUNCTION_ARGS) +{ + int n_parallel = PG_ARGISNULL(0) ? 1 : PG_GETARG_INT32(0); + int rounds = PG_ARGISNULL(1) ? 1 : PG_GETARG_INT64(1); + int iterations = PG_ARGISNULL(2) ? 128 : PG_GETARG_INT64(2); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + int proc_id; + + if (n_parallel <= 0 || rounds <= 0 || iterations <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("n_parallel, rounds, and iterations must be positive"))); + + InitMaterializedSRF(fcinfo, 0); + + proc_id = replicate_backend(n_parallel, rounds, iterations, 2, + microbench_parallel_work); + run_bufmap_bench(proc_id, n_parallel, rounds, iterations, rsinfo); + microbench_mp_leave(rsinfo); + + return (Datum) 0; +} diff --git a/src/test/modules/microbench/bufmap/install.sql b/src/test/modules/microbench/bufmap/install.sql new file mode 100644 index 00000000000..4d5fa39e096 --- /dev/null +++ b/src/test/modules/microbench/bufmap/install.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION bench_bufmap( + IN n_parallel int4 DEFAULT 1, + IN rounds int8 DEFAULT 1, + IN iterations int8 DEFAULT 128 +) +RETURNS SETOF microbench_sample +AS 'MODULE_PATHNAME', 'bench_bufmap' +LANGUAGE C; + +REVOKE ALL ON FUNCTION bench_bufmap(int4, int8, int8) FROM PUBLIC; diff --git a/src/test/modules/microbench/bufmap/query.sql b/src/test/modules/microbench/bufmap/query.sql new file mode 100644 index 00000000000..fe1c32c9809 --- /dev/null +++ b/src/test/modules/microbench/bufmap/query.sql @@ -0,0 +1,35 @@ +-- bufmap micro-benchmark query +-- +-- 1 row = 1 buffer: pad is STORAGE PLAIN and larger than half a page. +-- Grow anchor to 90% of shared_buffers if needed. iterations stays the +-- runner value so n_parallel * iterations fits in that heap. + +\pset format aligned + +\if :{?rounds} +\else +\set rounds 1000 +\endif +\if :{?iterations} +\else +\set iterations 128 +\endif +\if :{?max_parallel} +\else +\set max_parallel 10 +\endif + +\timing on + +SELECT format($q$ +SELECT %s AS workers, +op || coalesce(' / ' || "group"::text, '') as "op / wait" +, avg, q1, med, q3, count +FROM format_microbench_with_count( + (SELECT array_agg(s ORDER BY s.id)s + FROM bench_bufmap(%s, %s::int8, (%s / %s)::int8) AS s) +) bench_stats +ORDER BY 1,2; +$q$, i, i, :rounds, :iterations, i) +FROM generate_series(1, :max_parallel) AS i +\gexec diff --git a/src/test/modules/microbench/lwlock/bench.c b/src/test/modules/microbench/lwlock/bench.c new file mode 100644 index 00000000000..101cd7423c1 --- /dev/null +++ b/src/test/modules/microbench/lwlock/bench.c @@ -0,0 +1,135 @@ +/*------------------------------------------------------------------------- + * + * 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 "multiprocessing.h" +#include "randomize.h" +#include "timing-magic.h" + + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(bench_lwlock); + +static void +run_lwlock_bench(int proc_id, int n_parallel, int rounds, int iterations, + ReturnSetInfo *rsinfo) +{ + LWLock **locks; + pg_prng_state rng; + volatile int64 sink PG_USED_FOR_ASSERTS_ONLY = 0; + + (void) n_parallel; + + locks = palloc(sizeof(LWLock *) * iterations); + pg_prng_seed(&rng, 0xDA7ABA5E ^ (uint64) proc_id * 0x5EED); + for (int i = 0; i < iterations; i++) + locks[i] = BufMappingPartitionLock(i); + + if (!timing_initialized) + pg_initialize_timing(); + + for (int64 r = 0; r < rounds; r++) + { + INIT_TIMING_SCOPE(); + + /* + * Shuffling every round to reduce collision distribution bias + */ + shuffle_pointers(&rng, (void **) locks, iterations); + { + BufferDesc *buf_desc = GetBufferDescriptor(1); + + BEGIN_GROUPED_TIMING("spin-lock", iterations, 2) + { + LockBufHdr(buf_desc); + UnlockBufHdr(buf_desc); + } + END_GROUPED_TIMING; + } + + BEGIN_GROUPED_TIMING("LWLock-ex", iterations, 2) + { + LWLock *lock = locks[i]; + + group_id = LWLockAcquire(lock, LW_EXCLUSIVE) ? 0 : 1; + LWLockRelease(lock); + } + END_GROUPED_TIMING; + + + BEGIN_GROUPED_TIMING("LWLock-cond", iterations, 2) + { + LWLock *lock = locks[i]; + + if (LWLockConditionalAcquire(lock, LW_EXCLUSIVE)) + { + group_id = 0; + LWLockRelease(lock); + } + else + group_id = 1; + } + END_GROUPED_TIMING; + + BEGIN_GROUPED_TIMING("nop", iterations, 2) + { + LWLock *lock = locks[i]; + + sink += (int64) (uintptr_t) lock; + } + END_GROUPED_TIMING; + } + + (void) sink; +} + +static void +microbench_parallel_work(int proc_id, int n_parallel, int rounds, int iterations) +{ + run_lwlock_bench(proc_id, n_parallel, rounds, iterations, NULL); +} + +/* + * bench_lwlock(n_parallel, rounds, iterations) -> SETOF + * (op text, avg_ns float8, batch_size int8, id int8, group int8) + */ +Datum +bench_lwlock(PG_FUNCTION_ARGS) +{ + int n_parallel = PG_ARGISNULL(0) ? 1 : PG_GETARG_INT32(0); + int rounds = PG_ARGISNULL(1) ? 1 : PG_GETARG_INT64(1); + int iterations = PG_ARGISNULL(2) ? 128 : PG_GETARG_INT64(2); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + int proc_id; + + if (n_parallel <= 0 || rounds <= 0 || iterations <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("n_parallel, rounds, and iterations must be positive"))); + + InitMaterializedSRF(fcinfo, 0); + + proc_id = replicate_backend(n_parallel, rounds, iterations, 2, + microbench_parallel_work); + run_lwlock_bench(proc_id, n_parallel, rounds, iterations, rsinfo); + microbench_mp_leave(rsinfo); + + 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..2aa02c42101 --- /dev/null +++ b/src/test/modules/microbench/lwlock/install.sql @@ -0,0 +1,10 @@ +CREATE FUNCTION bench_lwlock( + IN n_parallel int4 DEFAULT 1, + IN rounds int8 DEFAULT 1, + IN iterations int8 DEFAULT 128 +) +RETURNS SETOF microbench_sample +AS 'MODULE_PATHNAME', 'bench_lwlock' +LANGUAGE C; + +REVOKE ALL ON FUNCTION bench_lwlock(int4, int8, int8) 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..12145d349dd --- /dev/null +++ b/src/test/modules/microbench/lwlock/query.sql @@ -0,0 +1,30 @@ +-- lwlock micro-benchmark query +-- +-- :rounds and :iterations are psql variables (run-test.sh sets them). +-- They are not expanded inside dollar-quoted strings. \gexec runs one +-- SELECT per n_parallel so each result set prints as soon as that size +-- finishes. + +\pset format aligned + +\if :{?rounds} +\else +\set rounds 1000 +\endif +\if :{?iterations} +\else +\set iterations 128 +\endif + + +\timing on + +SELECT format($q$ +SELECT %s AS n_parallel, bench_stats.* +FROM format_microbench_with_count( + (SELECT array_agg(s ORDER BY s.id) + FROM bench_lwlock(%s, %s::int8, %s::int8) AS s) +) bench_stats; +$q$, i, i, :rounds, :iterations) +FROM generate_series(1, 4) AS i +\gexec diff --git a/src/test/modules/microbench/meson.build b/src/test/modules/microbench/meson.build new file mode 100644 index 00000000000..0ac64174e70 --- /dev/null +++ b/src/test/modules/microbench/meson.build @@ -0,0 +1,35 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +microbench_tests = [ + 'lwlock', +] + +microbench_sources = files( + 'multiprocessing.c', +) + 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--1.0.sql b/src/test/modules/microbench/microbench--1.0.sql new file mode 100644 index 00000000000..5ab46100669 --- /dev/null +++ b/src/test/modules/microbench/microbench--1.0.sql @@ -0,0 +1,103 @@ +/* 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, + "group" int8 +); + +CREATE TYPE microbench_stats AS ( + op text, + "group" int8, + avg microbench_format, + min microbench_format, + q1 microbench_format, + med microbench_format, + q3 microbench_format, + max microbench_format, + std microbench_format +); + + +CREATE TYPE microbench_stats_with_count AS ( + op text, + "group" int8, + avg microbench_format, + min microbench_format, + q1 microbench_format, + med microbench_format, + q3 microbench_format, + max microbench_format, + std microbench_format, + count int8 +); + +CREATE FUNCTION format_microbench(samples microbench_sample[]) +RETURNS SETOF microbench_stats +LANGUAGE sql +STABLE +AS $$ + SELECT + s.op, + s."group", + sum(s.avg_ns * s.batch_size) / NULLIF(sum(s.batch_size), 0), + 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, s."group" + ORDER BY min(s.id), s."group" NULLS FIRST; +$$; + + +CREATE FUNCTION format_microbench_with_count(samples microbench_sample[]) +RETURNS SETOF microbench_stats_with_count +LANGUAGE sql +STABLE +AS $$ + SELECT + s.op, + s."group", + sum(s.avg_ns * s.batch_size) / NULLIF(sum(s.batch_size), 0), + 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), + sum(s.batch_size) + FROM unnest(samples) AS s + GROUP BY s.op, s."group" + ORDER BY min(s.id), s."group" NULLS FIRST; +$$; +CREATE FUNCTION bench_bufmap( + IN n_parallel int4 DEFAULT 1, + IN rounds int8 DEFAULT 1, + IN iterations int8 DEFAULT 128 +) +RETURNS SETOF microbench_sample +AS 'MODULE_PATHNAME', 'bench_bufmap' +LANGUAGE C; + +REVOKE ALL ON FUNCTION bench_bufmap(int4, int8, int8) FROM PUBLIC; +CREATE FUNCTION bench_lwlock( + IN n_parallel int4 DEFAULT 1, + IN rounds int8 DEFAULT 1, + IN iterations int8 DEFAULT 128 +) +RETURNS SETOF microbench_sample +AS 'MODULE_PATHNAME', 'bench_lwlock' +LANGUAGE C; + +REVOKE ALL ON FUNCTION bench_lwlock(int4, int8, int8) FROM PUBLIC; 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..d74a6442fca --- /dev/null +++ b/src/test/modules/microbench/microbench-head--1.0.sql @@ -0,0 +1,83 @@ +/* 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, + "group" int8 +); + +CREATE TYPE microbench_stats AS ( + op text, + "group" int8, + avg microbench_format, + min microbench_format, + q1 microbench_format, + med microbench_format, + q3 microbench_format, + max microbench_format, + std microbench_format +); + + +CREATE TYPE microbench_stats_with_count AS ( + op text, + "group" int8, + avg microbench_format, + min microbench_format, + q1 microbench_format, + med microbench_format, + q3 microbench_format, + max microbench_format, + std microbench_format, + count int8 +); + +CREATE FUNCTION format_microbench(samples microbench_sample[]) +RETURNS SETOF microbench_stats +LANGUAGE sql +STABLE +AS $$ + SELECT + s.op, + s."group", + sum(s.avg_ns * s.batch_size) / NULLIF(sum(s.batch_size), 0), + 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, s."group" + ORDER BY min(s.id), s."group" NULLS FIRST; +$$; + + +CREATE FUNCTION format_microbench_with_count(samples microbench_sample[]) +RETURNS SETOF microbench_stats_with_count +LANGUAGE sql +STABLE +AS $$ + SELECT + s.op, + s."group", + sum(s.avg_ns * s.batch_size) / NULLIF(sum(s.batch_size), 0), + 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), + sum(s.batch_size) + FROM unnest(samples) AS s + GROUP BY s.op, s."group" + ORDER BY min(s.id), s."group" NULLS FIRST; +$$; 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/multiprocessing.c b/src/test/modules/microbench/multiprocessing.c new file mode 100644 index 00000000000..deb1dba399a --- /dev/null +++ b/src/test/modules/microbench/multiprocessing.c @@ -0,0 +1,312 @@ +/*------------------------------------------------------------------------- + * + * multiprocessing.c + * Launch and synchronize micro-benchmark parallel workers. + * + * Follows the parallel btree-build pattern: EnterParallelMode, + * CreateParallelContext, shm_toc, LaunchParallelWorkers, + * WaitForParallelWorkersToFinish. Cooperating backends rendezvous with + * an atomic spin barrier (not BarrierArriveAndWait) so sync stays off + * the kernel path. Worker timing rows are stored in DSM and copied + * into the leader tuplestore at each synchronize and at leave. + * + * IDENTIFICATION + * src/test/modules/microbench/multiprocessing.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "access/parallel.h" +#include "access/xact.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "port/atomics.h" +#include "storage/s_lock.h" +#include "utils/builtins.h" +#include "utils/tuplestore.h" + +#include "multiprocessing.h" + +#define PARALLEL_KEY_MICROBENCH_SHARED UINT64CONST(0xA1100001) +#define MICROBENCH_MAX_OPS_PER_ROUND 32 +#define MICROBENCH_OP_LEN 64 + +typedef struct MicrobenchSample +{ + char op[MICROBENCH_OP_LEN]; + double avg_ns; + int64 batch_size; + int64 id; + int64 group; + bool group_isnull; +} MicrobenchSample; + +typedef struct MicrobenchShared +{ + pg_atomic_uint32 ready; + pg_atomic_uint32 nsamples; + pg_atomic_uint32 arrived; + pg_atomic_uint32 generation; + int n_parallel; + int rounds; + int iterations; + int max_group_size; + int max_samples; + ptrdiff_t work_off; + MicrobenchSample samples[FLEXIBLE_ARRAY_MEMBER]; +} MicrobenchShared; + +extern PGDLLEXPORT void microbench_parallel_main(dsm_segment *seg, shm_toc *toc); + +static MicrobenchShared *microbench_mp_state = NULL; +static ParallelContext *microbench_mp_pcxt = NULL; +static int microbench_mp_id = 0; + +static void +microbench_mp_flush_samples(ReturnSetInfo *rsinfo) +{ + MicrobenchShared *shared = microbench_mp_state; + uint32 n; + uint32 i; + Datum values[5]; + bool nulls[5] = {0}; + + if (shared == NULL || rsinfo == NULL || rsinfo->setResult == NULL) + return; + + n = pg_atomic_read_u32(&shared->nsamples); + if (n > (uint32) shared->max_samples) + n = (uint32) shared->max_samples; + + for (i = 0; i < n; i++) + { + MicrobenchSample *s = &shared->samples[i]; + + values[0] = CStringGetTextDatum(s->op); + values[1] = Float8GetDatum(s->avg_ns); + values[2] = Int64GetDatum(s->batch_size); + values[3] = Int64GetDatum(s->id); + values[4] = Int64GetDatum(s->group); + nulls[4] = s->group_isnull; + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + if(!pg_atomic_compare_exchange_u32(&shared->nsamples, &n, 0)) + { + elog(FATAL, "Race condition while flushing results."); + } +} + +static void +microbench_mp_teardown(ReturnSetInfo *rsinfo) +{ + if (IsParallelWorker()) + return; + + if (microbench_mp_pcxt != NULL) + { + WaitForParallelWorkersToFinish(microbench_mp_pcxt); + microbench_mp_flush_samples(rsinfo); + DestroyParallelContext(microbench_mp_pcxt); + microbench_mp_pcxt = NULL; + ExitParallelMode(); + } + + microbench_mp_state = NULL; + microbench_mp_id = 0; +} + +bool +microbench_mp_recording(void) +{ + return microbench_mp_state != NULL && microbench_mp_state->n_parallel > 1; +} + +void +microbench_mp_emit_sample(ReturnSetInfo *rsinfo, const char *op, + double avg_ns, int64 batch_size, int64 id, + int64 group, bool group_isnull) +{ + if (rsinfo == NULL) + { + MicrobenchShared *shared = microbench_mp_state; + uint32 slot; + + slot = pg_atomic_fetch_add_u32(&shared->nsamples, 1); + if (slot >= (uint32) shared->max_samples) + elog(ERROR, "microbench sample buffer overflow"); + + strlcpy(shared->samples[slot].op, op, MICROBENCH_OP_LEN); + shared->samples[slot].avg_ns = avg_ns; + shared->samples[slot].batch_size = batch_size; + shared->samples[slot].id = id; + shared->samples[slot].group = group; + shared->samples[slot].group_isnull = group_isnull; + } + else if (rsinfo != NULL && rsinfo->setResult != NULL) + { + Datum values[5]; + bool nulls[5] = {0}; + + values[0] = CStringGetTextDatum(op); + values[1] = Float8GetDatum(avg_ns); + values[2] = Int64GetDatum(batch_size); + values[3] = Int64GetDatum(id); + values[4] = Int64GetDatum(group); + nulls[4] = group_isnull; + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } +} + +int +replicate_backend(int n_parallel, int rounds, int iterations, int groups, + microbench_parallel_work_fn work) +{ + ParallelContext *pcxt; + MicrobenchShared *shared; + int nworkers = n_parallel - 1; + int max_samples = nworkers * groups; + int shared_size = offsetof(MicrobenchShared, samples) + + max_samples * sizeof(MicrobenchSample); + if (n_parallel <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("n_parallel must be positive"))); + + if (n_parallel <= 1) + { + microbench_mp_id = 1; + return 1; + } + + if (microbench_mp_id != 0) + return microbench_mp_id; + + EnterParallelMode(); + pcxt = CreateParallelContext("microbench", "microbench_parallel_main", + nworkers); + shm_toc_estimate_chunk(&pcxt->estimator, shared_size); + shm_toc_estimate_keys(&pcxt->estimator, 1); + InitializeParallelDSM(pcxt); + + if (pcxt->seg == NULL) + { + DestroyParallelContext(pcxt); + ExitParallelMode(); + microbench_mp_id = 1; + return 1; + } + + shared = (MicrobenchShared *) shm_toc_allocate(pcxt->toc, shared_size); + memset(shared, 0, shared_size); + pg_atomic_init_u32(&shared->ready, 0); + pg_atomic_init_u32(&shared->nsamples, 0); + pg_atomic_init_u32(&shared->arrived, 0); + pg_atomic_init_u32(&shared->generation, 0); + shared->max_samples = max_samples; + shared->n_parallel = n_parallel; + shared->rounds = rounds; + shared->iterations = iterations; + shared->work_off = (uintptr_t) work - (uintptr_t) microbench_parallel_main; + shm_toc_insert(pcxt->toc, PARALLEL_KEY_MICROBENCH_SHARED, shared); + + LaunchParallelWorkers(pcxt); + + if (pcxt->nworkers_launched < nworkers) + { + int got = pcxt->nworkers_launched; + + DestroyParallelContext(pcxt); + ExitParallelMode(); + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("could not launch %d parallel workers (got %d)", + nworkers, got), + errhint("Increase max_worker_processes and max_parallel_workers."))); + } + + microbench_mp_state = shared; + microbench_mp_pcxt = pcxt; + microbench_mp_id = 1; + pg_atomic_write_membarrier_u32(&shared->ready, 1); + + WaitForParallelWorkersToAttach(pcxt); + return 1; +} + +static void +wait_eq(volatile pg_atomic_uint32 *ptr, uint32 value) +{ + uint32 spins = 0; + + while (pg_atomic_read_membarrier_u32(ptr) != value) + { + SPIN_DELAY(); + if ((++spins & 0xFFFF) == 0) + CHECK_FOR_INTERRUPTS(); + } +} + +void +synchronize_backends(ReturnSetInfo *rsinfo) +{ + MicrobenchShared *shared = microbench_mp_state; + uint32 gen; + + if (shared == NULL || shared->n_parallel <= 1) + return; + + /* + * Sense-reversing spin barrier. Capture the generation first, then + * announce arrival. The leader waits until everyone is here, copies + * samples, then advances the generation so waiters return together. + */ + gen = pg_atomic_read_u32(&shared->generation); + pg_atomic_add_fetch_u32(&shared->arrived, 1); + + if (!IsParallelWorker()) + { + wait_eq(&shared->arrived, (uint32) shared->n_parallel); + microbench_mp_flush_samples(rsinfo); + pg_atomic_write_u32(&shared->arrived, 0); + pg_atomic_write_membarrier_u32(&shared->generation, gen + 1); + } + else + wait_eq(&shared->generation, gen + 1); +} + +void +microbench_mp_leave(ReturnSetInfo *rsinfo) +{ + microbench_mp_teardown(rsinfo); +} + +/* + * Parallel-worker entry point. Looked up by name in the worker process, + * the same way _bt_parallel_build_main is. + */ +PGDLLEXPORT void +microbench_parallel_main(dsm_segment *seg, shm_toc *toc) +{ + MicrobenchShared *shared; + microbench_parallel_work_fn work; + uint32 spins = 0; + + shared = shm_toc_lookup(toc, PARALLEL_KEY_MICROBENCH_SHARED, false); + + while (pg_atomic_read_u32(&shared->ready) == 0) + { + SPIN_DELAY(); + if ((++spins & 0xFFFF) == 0) + CHECK_FOR_INTERRUPTS(); + } + + microbench_mp_state = shared; + microbench_mp_id = ParallelWorkerNumber + 2; + work = (microbench_parallel_work_fn) + ((uintptr_t) microbench_parallel_main + shared->work_off); + work(microbench_mp_id, shared->n_parallel, + shared->rounds, shared->iterations); +} diff --git a/src/test/modules/microbench/multiprocessing.h b/src/test/modules/microbench/multiprocessing.h new file mode 100644 index 00000000000..4d8c2ac0e01 --- /dev/null +++ b/src/test/modules/microbench/multiprocessing.h @@ -0,0 +1,30 @@ +#ifndef MICROBENCH_MULTIPROCESSING_H +#define MICROBENCH_MULTIPROCESSING_H + +struct ReturnSetInfo; + +typedef void (*microbench_parallel_work_fn) (int proc_id, int n_parallel, + int rounds, int iterations); + +/* + * Launch n-1 parallel workers (same infrastructure as parallel index + * builds) and return 1 in the leader. Workers enter through + * microbench_parallel_main and call the work function passed here. + * + * synchronize_backends() spins until the whole party has arrived, the + * leader flushes new DSM samples, then it releases everyone. It is a + * no-op when n_parallel <= 1. microbench_mp_leave() waits for workers + * and flushes the last batch, then exits parallel mode. + */ +extern int replicate_backend(int n_parallel, int rounds, int iterations, + int samples_per_round, + microbench_parallel_work_fn work); +extern void synchronize_backends(struct ReturnSetInfo *rsinfo); +extern void microbench_mp_leave(struct ReturnSetInfo *rsinfo); +extern bool microbench_mp_recording(void); +extern void microbench_mp_emit_sample(struct ReturnSetInfo *rsinfo, + const char *op, double avg_ns, + int64 batch_size, int64 id, + int64 group, bool group_isnull); + +#endif diff --git a/src/test/modules/microbench/randomize.h b/src/test/modules/microbench/randomize.h new file mode 100644 index 00000000000..f497015a79a --- /dev/null +++ b/src/test/modules/microbench/randomize.h @@ -0,0 +1,26 @@ +#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 + * + * Integer indexes (e.g. BlockNumber) can be stored in the array as intptr_t + * and shuffled with the same function. + */ +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..f846a17e23c --- /dev/null +++ b/src/test/modules/microbench/scripts/run-test.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Run one microbench test folder. +# +# Usage: run-test.sh TEST +# Env: TOP_BUILDDIR, PG_CONFIG, MICROBENCH_PORT (default 55432), +# MICROBENCH_PARAMS +# +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" +ROUNDS=${MICROBENCH_ROUNDS:-1000} +MICROBENCH_PARAMS=${MIBROBENCH_PARAMS:""} +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 -c max_worker_processes=256 -c max_parallel_workers=256" 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 -c max_worker_processes=256 -c max_parallel_workers=256" 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" +"$BINDIR/psql" -v ON_ERROR_STOP=1 -p "$PORT" -d postgres \ + $MICROBENCH_PARAMS \ + -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..15f3978d49c --- /dev/null +++ b/src/test/modules/microbench/timing-magic.h @@ -0,0 +1,73 @@ +#ifndef MICROBENCH_TIMING_MAGIC_H +#define MICROBENCH_TIMING_MAGIC_H + +#include + +#include "portability/instr_time.h" +#include "multiprocessing.h" + +#define INIT_TIMING_SCOPE() \ + int64 timing_operation_id = 0 + +#define BEGIN_TIMING(name, iterations) \ + do { \ + instr_time t0, t1, dt; \ + const char *timing_name = (name); \ + double avg_ns; \ + synchronize_backends(rsinfo); \ + INSTR_TIME_SET_CURRENT(t0); \ + for (int64 i = 0; i < (iterations); ++i) \ + { + +#define END_TIMING \ + } \ + INSTR_TIME_SET_CURRENT(t1); \ + INSTR_TIME_SET_ZERO(dt); \ + INSTR_TIME_ACCUM_DIFF(dt, t1, t0); \ + avg_ns = (double) INSTR_TIME_GET_NANOSEC(dt) / (double) (iterations); \ + microbench_mp_emit_sample(rsinfo, timing_name, avg_ns, (iterations), \ + ++timing_operation_id, 0, true); \ + } while (0) + +#define BEGIN_GROUPED_TIMING(name, iterations, n_groups) \ + do { \ + int64 _n_groups = (n_groups); \ + int64 _n = (iterations); \ + int64 group_count[n_groups]; \ + instr_time group_dt[n_groups]; \ + instr_time t0, t1; \ + const char *timing_name = (name); \ + int64 g; \ + int64 i; \ + memset(group_count, 0, sizeof(group_count)); \ + for (g = 0; g < _n_groups; g++) \ + INSTR_TIME_SET_ZERO(group_dt[g]); \ + synchronize_backends(rsinfo); \ + for (i = 0; i < _n; ++i) \ + { \ + int64 group_id = 0; \ + INSTR_TIME_SET_CURRENT(t0); + +#define END_GROUPED_TIMING \ + INSTR_TIME_SET_CURRENT(t1); \ + if (group_id < 0 || group_id >= _n_groups) \ + elog(ERROR, \ + "group_id " INT64_FORMAT " out of range [0, " INT64_FORMAT ")", \ + group_id, _n_groups); \ + INSTR_TIME_ACCUM_DIFF(group_dt[group_id], t1, t0); \ + group_count[group_id]++; \ + } \ + for (g = 0; g < _n_groups; g++) \ + { \ + int64 cnt; \ + double avg_ns; \ + cnt = group_count[g]; \ + if (cnt == 0) \ + continue; \ + avg_ns = (double) INSTR_TIME_GET_NANOSEC(group_dt[g]) / (double) cnt; \ + microbench_mp_emit_sample(rsinfo, timing_name, avg_ns, cnt, \ + ++timing_operation_id, g, false); \ + } \ + } while (0) + +#endif -- 2.53.0