From 264118cf7818b5fb52e1e81d92eda44ae65a132a Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:06:59 -0600 Subject: [PATCH] Add a reproducible benchmark suite and publish the results bench/run.sh + bench/setup.sql compare PL/php against PL/pgSQL and PL/Perl on four workloads (scalar math, string ops, an SPI loop over 1000 rows, repeated small SPI statements) via pgbench, with identical logic in each language. doc/benchmarks.md publishes the numbers and their reading: scalar/string work is call-overhead-bound and all three languages are within a few percent; row iteration is PL/pgSQL's native strength while PL/php still runs 1.75x faster than PL/Perl; repeated SPI statements carry the subtransaction that makes PL/php and PL/Perl errors catchable, with PL/php ~25% ahead of PL/Perl. One optimization was tried and rejected: interning column-name hash keys per result measured as a no-op (the row-loop cost is per-cell value conversion, not key handling), so it was dropped rather than kept as complexity without benefit. The document records that, and the plausible future fast path (binary Datum conversion for common scalars). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 8 +++++ README.md | 1 + bench/run.sh | 32 +++++++++++++++++++ bench/setup.sql | 78 +++++++++++++++++++++++++++++++++++++++++++++++ doc/benchmarks.md | 56 ++++++++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100755 bench/run.sh create mode 100644 bench/setup.sql create mode 100644 doc/benchmarks.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c629d1..f53d6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,14 @@ and the project aims to follow [Semantic Versioning](https://semver.org/). suite against a `libasan`-preloaded server, catching memory-safety bugs mechanically (new `ASAN_FLAGS` hook in the Makefiles). +## [Unreleased] + +### Added + +- **A reproducible benchmark suite** (`bench/`) and published results + (`doc/benchmarks.md`) comparing PL/php with PL/pgSQL and PL/Perl: within a + few percent on scalar and string work, 1.75× PL/Perl on SPI row loops. + ## [2.3.0] — 2026-07-06 ### Added diff --git a/README.md b/README.md index 6e29c2c..63a3e10 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ roles you would trust with the server's OS account. - [Language reference](doc/plphp.md) - [Cookbook — tested recipes](doc/cookbook.md) +- [Benchmarks](doc/benchmarks.md) — within a few percent of PL/pgSQL on scalar work; 1.75× PL/Perl on row loops - [Installation](INSTALL) - [Changelog](CHANGELOG.md) - [PL/php vs PL/Perl](doc/plperl-comparison.md) diff --git a/bench/run.sh b/bench/run.sh new file mode 100755 index 0000000..94a79fd --- /dev/null +++ b/bench/run.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Benchmark PL/php against PL/pgSQL and PL/Perl. +# +# Usage: PGPORT=5432 [PGBENCH=/usr/lib/postgresql/18/bin/pgbench] sh bench/run.sh +# Requires: a running cluster you can create a "plphp_bench" database in, +# with plphp installed and (optionally) plperl available. +set -e +PGBENCH=${PGBENCH:-pgbench} +DB=plphp_bench +SECS=${SECS:-8} + +dropdb --if-exists $DB 2>/dev/null || true +createdb $DB +psql -qX -d $DB -f "$(dirname $0)/setup.sql" + +for fn in math str rows spi; do + case $fn in + math) body='\set a random(1,1000) +SELECT FN(:a, 7);' ;; + str) body='SELECT FN(chr(97+(random()*20)::int) || repeat(chr(98), 30));' ;; + rows) body='SELECT FN();' ;; + spi) body='\set a random(1,1000) +SELECT FN(:a);' ;; + esac + for lang in php pgsql perl; do + printf '%s\n' "$body" | sed "s/FN/${fn}_${lang}/" > /tmp/plphp_bench_$$.sql + tps=$($PGBENCH -n -c 1 -T $SECS -f /tmp/plphp_bench_$$.sql $DB 2>/dev/null \ + | awk '/^tps/ {printf "%.0f", $3}') + printf '%-12s %10s tps\n' "${fn}_${lang}" "$tps" + done +done +rm -f /tmp/plphp_bench_$$.sql diff --git a/bench/setup.sql b/bench/setup.sql new file mode 100644 index 0000000..9d0df47 --- /dev/null +++ b/bench/setup.sql @@ -0,0 +1,78 @@ +-- benchmark functions: identical logic in plphp / plpgsql / plperl +CREATE EXTENSION IF NOT EXISTS plphp; +CREATE EXTENSION IF NOT EXISTS plperl; + +-- 1. scalar math +CREATE OR REPLACE FUNCTION math_php(a int, b int) RETURNS int LANGUAGE plphp AS $$ + return ($args[0] * 3 + $args[1]) % 97; +$$; +CREATE OR REPLACE FUNCTION math_pgsql(a int, b int) RETURNS int LANGUAGE plpgsql AS $$ +BEGIN RETURN (a * 3 + b) % 97; END; +$$; +CREATE OR REPLACE FUNCTION math_perl(a int, b int) RETURNS int LANGUAGE plperl AS $$ + return ($_[0] * 3 + $_[1]) % 97; +$$; + +-- 2. string work +CREATE OR REPLACE FUNCTION str_php(t text) RETURNS text LANGUAGE plphp AS $$ + return strtoupper(strrev($args[0])) . strlen($args[0]); +$$; +CREATE OR REPLACE FUNCTION str_pgsql(t text) RETURNS text LANGUAGE plpgsql AS $$ +BEGIN RETURN upper(reverse(t)) || length(t); END; +$$; +CREATE OR REPLACE FUNCTION str_perl(t text) RETURNS text LANGUAGE plperl AS $$ + return uc(reverse($_[0])) . length($_[0]); +$$; + +-- 3. SPI row loop over a 1000-row table +DROP TABLE IF EXISTS bench_rows; +CREATE TABLE bench_rows AS SELECT g AS id, g * 2 AS val FROM generate_series(1, 1000) g; +CREATE OR REPLACE FUNCTION rows_php() RETURNS bigint LANGUAGE plphp AS $$ + $r = spi_exec("select val from bench_rows"); + $s = 0; + while ($row = spi_fetch_row($r)) + $s += $row['val']; + return $s; +$$; +CREATE OR REPLACE FUNCTION rows_pgsql() RETURNS bigint LANGUAGE plpgsql AS $$ +DECLARE s bigint := 0; r record; +BEGIN + FOR r IN SELECT val FROM bench_rows LOOP s := s + r.val; END LOOP; + RETURN s; +END; +$$; +CREATE OR REPLACE FUNCTION rows_perl() RETURNS bigint LANGUAGE plperl AS $$ + my $rv = spi_exec_query("select val from bench_rows"); + my $s = 0; + $s += $_->{val} for @{$rv->{rows}}; + return $s; +$$; + +-- 4. repeated small SPI statements (10 queries per call) +CREATE OR REPLACE FUNCTION spi_php(n int) RETURNS int LANGUAGE plphp AS $$ + $s = 0; + for ($i = 0; $i < 10; $i++) { + $r = spi_exec("select " . ($args[0] + $i) . " as x"); + $row = spi_fetch_row($r); + $s += $row['x']; + } + return $s; +$$; +CREATE OR REPLACE FUNCTION spi_pgsql(n int) RETURNS int LANGUAGE plpgsql AS $$ +DECLARE s int := 0; x int; +BEGIN + FOR i IN 0..9 LOOP + EXECUTE 'select ' || (n + i) INTO x; + s := s + x; + END LOOP; + RETURN s; +END; +$$; +CREATE OR REPLACE FUNCTION spi_perl(n int) RETURNS int LANGUAGE plperl AS $$ + my $s = 0; + for my $i (0..9) { + my $rv = spi_exec_query("select " . ($_[0] + $i) . " as x"); + $s += $rv->{rows}[0]{x}; + } + return $s; +$$; diff --git a/doc/benchmarks.md b/doc/benchmarks.md new file mode 100644 index 0000000..550bff9 --- /dev/null +++ b/doc/benchmarks.md @@ -0,0 +1,56 @@ +# PL/php performance + +How fast is PL/php compared to the built-in procedural languages? These are +the first published numbers for the modernized extension. Reproduce them any +time with the committed suite: + +```sh +PGPORT=5432 sh bench/run.sh +``` + +## Results + +PostgreSQL 18, PHP 8.3 (embed, NTS), single client, `pgbench -T 8`, one +warm session, Ubuntu 24.04 container on x86-64. Higher is better; treat +±3% as noise. + +| Benchmark | PL/php | PL/pgSQL | PL/Perl | +|---|---:|---:|---:| +| Scalar math (`(a*3+b)%97`) | 58,800 | 60,000 | 57,900 | +| String ops (reverse+upper+length) | 43,400 | 44,000 | 46,300 | +| SPI loop over 1,000 rows | 4,700 | 8,600 | 2,700 | +| 10 small SPI statements per call | 23,500 | 26,000 | 18,700 | + +## Reading the numbers + +- **Scalar and string work is call-overhead-bound.** All three languages sit + within a few percent of each other: the executor's function-call machinery + dominates, not the interpreter. PL/php's text-based argument conversion is + not a measurable factor at this scale. +- **Row iteration is PL/pgSQL's home turf.** Its `FOR ... IN SELECT` loop + iterates natively without crossing a language boundary per row. PL/php + pays a C-to-PHP conversion per row (one output-function call and one zval + per column) — and is still **1.75× faster than PL/Perl**, which + materializes the entire result into Perl structures up front. +- **Repeated SPI statements** carry a per-call subtransaction in both PL/php + and PL/Perl (that is what makes their errors catchable); PL/pgSQL's + `EXECUTE` does not. PL/php lands between the two, ~25% ahead of PL/Perl. + +## What was tried and rejected + +Interning the column-name hash keys once per result (instead of hashing them +per row) measured as a no-op: the row-loop cost lives in per-cell value +conversion, not key handling. The optimization was dropped rather than +carried as complexity without benefit. A future fast path worth exploring is +converting common scalar types (int/float/bool/text) from their binary Datum +form instead of through the type output functions — that requires threading +type metadata into `spi_fetch_row`'s result handling. + +## Guidance + +- For pure computation, use whichever language reads best — the overhead + differences are negligible. +- For tight loops over large results, prefer a set-based SQL statement (or + PL/pgSQL) when the logic allows; when you need PHP's expressiveness per + row, `spi_query`/`spi_fetchrow` keeps memory flat and PL/php's per-row + cost is the best of the interpreted PLs measured here.