[php-src] PR #24565: ext/pdo: Size PDO::FETCH_BOTH rows for both keys of each column
| From: | brainexe | Date: | Thu, 22 Oct 2026 17:39:36 +0000 |
| Subject: | [php-src] PR #24565: ext/pdo: Size PDO::FETCH_BOTH rows for both keys of each column | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39729@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24565
Author: brainexe
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.