Skip to content

Speed up zend_hash_iterator_add() when iterators are nested - #23947

Open
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:hash-iterator-add
Open

nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:hash-iterator-add

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

zend_hash_iterator_add() looks for a free slot in EG(ht_iterators) starting from slot 0 every time, so a foreach by reference, a foreach over object properties, an ArrayIterator or array_walk() pays one probe per iterator that's already alive.

This adds EG(ht_iterators_free_hint): all slots below it are in use, zend_hash_iterator_add() starts looking from there and zend_hash_iterator_del() lowers it. The returned slot is still the lowest free one, so slot indexes and EG(ht_iterators_used) don't change. 51451ab tried scanning backwards instead, and was reverted the next day by ceb77b6; keeping first-fit avoids changing which slot gets reused.

Found while profiling #23945: a recursive walk of 17.6k Twig nodes, each iterated through new ArrayIterator($this->nodes), spends 2.4M instructions in zend_hash_iterator_add()/del(). That's 1.5M with this patch, -1.2% on the whole walk (callgrind).

Instructions per foreach ($a as &$v) {} over a 1-element array, nested in d-1 other loops by reference: 364 -> 365 at d=1, 389 -> 370 at d=5, 464 -> 369 at d=20. Same savings per loop for ArrayIterator, foreach over properties and array_walk().

zend_hash_iterator_add() scanned EG(ht_iterators) from slot 0 on every
call, so each nested foreach by reference, foreach over object
properties, ArrayIterator or array_walk() paid one probe per iterator
already alive.

EG(ht_iterators_free_hint) tracks that all slots below it are in use:
zend_hash_iterator_add() starts looking from there and
zend_hash_iterator_del() lowers it. The returned slot is still the
lowest free one, so slot indexes and EG(ht_iterators_used) are the same
as before.
@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

Rebased. I also removed the NEWS and UPGRADING entries, since they were the only cause of the conflicts.

@ndossche @iluuu1994 would one of you have time for a look? Nothing changes when a single iterator is alive. When loops are nested, zend_hash_iterator_add() doesn't scan the slots in use anymore. An empty foreach by reference then needs 20% fewer instructions at depth 20.

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.

1 participant