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.