Skip to content

Use zend_hash_lookup() when unserializing arrays on PHP 8.1+ (~20% faster for arrays) - #427

Open
brainexe wants to merge 1 commit into
igbinary:masterfrom
brainexe:unserialize-zend-hash-lookup
Open

brainexe wants to merge 1 commit into
igbinary:masterfrom
brainexe:unserialize-zend-hash-lookup

Conversation

@brainexe

Copy link
Copy Markdown

Summary

On PHP 8.1+, igbinary_zend_hash_add_or_find() / igbinary_zend_hash_index_add_or_find() fall back to the
"unoptimized reference implementation", which probes the hash table twice for every array element:
zend_hash_find() followed by zend_hash_add_new().

PHP 8.1 added zend_hash_lookup() / zend_hash_index_lookup() (Zend/zend_hash.h), which have exactly the
semantics igbinary needs here: return the existing value, or insert and return a new IS_NULL entry, in a single
probe. They are also what ext/standard's unserialize() uses for the same purpose
(ext/standard/var_unserializer.re, process_nested_array_data).

This PR uses them for PHP_VERSION_ID >= 80100. The pre-7.2 reference implementation and the 7.2–8.0
specialization are unchanged, and IGBINARY_FORCE_REFERENCE_HASH_TABLE_FUNCTIONS still forces the reference
implementation.

Also adds benchmark/unserialize-assocrowarray.b.php (an array of 200 associative arrays with 10 keys each,
i.e. typical database rows / cached API responses), since that shape was not covered by the existing benchmarks.

Benchmarks

PHP 8.4.27 (NTS, --disable-all, -O2), Linux x86_64, i7-13700H pinned to one performance core, best of 3
interleaved rounds. igbinary_unserialize() time per call:

payload master this PR change
200 assoc rows (10 keys, mixed scalars + small array) 34.4 us 27.1 us −21%
2000 assoc rows 344 us 275 us −20%
int list, 1000 elements 7.5 us 5.6 us −25%
int-keyed map, 1000 elements 11.0 us 8.0 us −27%
string list, 1000 elements 17.0 us 15.2 us −11%
string-keyed map, 1000 elements 23.5 us 21.3 us −9%
100 × 4-level nested arrays 28.7 us 24.4 us −15%
1000 references to one object 12.2 us 10.7 us −13%
200 objects with 10 typed properties 63.8 us 63.7 us 0% (object properties use a different path)

Existing benchmark/ scripts (seconds per run, min of 40 runs):

script master this PR
unserialize-intarray 0.0848 0.0737 (−13%)
unserialize-scalararray 0.0641 0.0535 (−17%)
unserialize-structured-array 0.3051 0.2695 (−12%)
unserialize-stringarray 0.1482 0.1442 (−3%)
unserialize-objectarray 0.0446 0.0445 (0%)
unserialize-assocrowarray (new) 0.1031 0.0819 (−21%)

A sampling profile of the 2000-assoc-row case on master showed zend_hash_add_new (12.8%) and zend_hash_find
(10.9%) as the two largest non-igbinary costs; with this change they are replaced by a single zend_hash_lookup
at 17%.

Testing

  • make test: 135 passed, 0 failed, 25 skipped on PHP 8.4.27 (release build), and the same result on a
    --enable-debug build.
  • Behavior is unchanged: both functions return the same zval * the old find-or-add pair returned, including
    IS_INDIRECT slots, which callers already handle.

igbinary_zend_hash_add_or_find()/igbinary_zend_hash_index_add_or_find()
fell back to the "unoptimized reference implementation" on PHP 8.1+,
which probes the hash table twice per array element (zend_hash_find()
followed by zend_hash_add_new()).

PHP 8.1 added zend_hash_lookup()/zend_hash_index_lookup(), which have
exactly the required semantics (return the existing value, or insert
and return a new IS_NULL entry) in a single probe. They are what
ext/standard's unserialize() uses as well.

Also add a benchmark for arrays of associative arrays (database rows).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant