Skip to content

ext/pdo: Size PDO::FETCH_BOTH rows for both keys of each column - #24565

Merged
iliaal merged 1 commit into
php:masterfrom
brainexe:perf/pdo-fetch-both-sizing
Oct 23, 2026
Merged

iliaal merged 1 commit into
php:masterfrom
brainexe:perf/pdo-fetch-both-sizing

Conversation

@brainexe

Copy link
Copy Markdown
Contributor

PDO::FETCH_BOTH is the default fetch mode. It stores every value twice, under the column name and under the column index, but do_fetch() created the row array with array_init_size(return_value, stmt->column_count), i.e. room for one entry per column. With the minimum hash size of 8, every row with 5 or more columns therefore filled its table halfway through and was resized and rehashed once, for every fetched row.

This PR sizes the array for 2 * column_count entries in FETCH_BOTH mode. The array ends up with the same size as before, it just gets there without the intermediate table. Other fetch modes are unchanged. (PDO_FETCH_USE_DEFAULT is resolved to the statement's default mode at the start of do_fetch(), so the default case is covered.)

Benchmark

Release build (-O2 -g -fno-omit-frame-pointer), opcache on, JIT off, pinned to one P-core, perf stat -r5, user-space counters. Compared to master a2e4d85bf31.

pdo_pgsql, fetchAll(PDO::FETCH_BOTH) of 10k rows with 11 columns (int8, text ×4, timestamptz ×2, numeric, int4, bool, float8; one nullable), 50 times, PostgreSQL 18 on localhost:

master this PR
instructions 4766M 4534M −4.9%
cycles 1408M 1278M −9.2%

pdo_sqlite, in-memory table with N columns (alternating integer and text), fetchAll(PDO::FETCH_BOTH) of 10k rows, 20 times. The numbers include creating and filling the table, so the relative gain is smaller than for the fetch alone:

columns master this PR
3 735.6M 732.5M ±0 (fits into 8 slots anyway)
4 933.6M 940.8M ±0 (noise)
5 1142.1M 1091.1M −4.5%
8 1678.9M 1624.5M −3.2%
11 2205.8M 2111.4M −4.3%
20 3822.2M 3675.2M −3.8%

Cycles move by −4.7% (5 columns) to −6.2% (20 columns). Controls: FETCH_ASSOC (pgsql 10k×11 and sqlite 11 columns) and a prepare/execute/fetch-one loop stay within the noise of ±1%.

Memory: unchanged. fetchAll(FETCH_BOTH) of the 10k×11 pgsql result retains 17.00 MB and peaks at 17.42 MB on both builds.

Verification

  • ext/pdo, ext/pdo_pgsql, ext/pdo_sqlite test suites pass. FETCH_BOTH is the default mode, so most existing tests exercise it, including duplicate and numeric column names.
  • Only the initial capacity changes; key order and contents are produced by the same code as before.

PDO::FETCH_BOTH, the default fetch mode, stores each value under its
column name and its index, but the row array was sized for one entry per
column. Rows with more than 4 columns were therefore resized and rehashed
once per fetched row. The final array size is unchanged.

50x fetchAll(PDO::FETCH_BOTH) of 10k rows with 11 columns (pdo_pgsql):
-4.9% instructions, -9% cycles

@iliaal iliaal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me, the only corner case (very rare) is when numeric column names are used and this would result and double allocation, but that is corner case and common case would benefit from this change.

@iliaal
iliaal merged commit 14c65d1 into php:master Oct 23, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants