Skip to content

Iterate treaps with an explicit stack instead of coroutines - #30

Merged
ericeil merged 2 commits into
mainfrom
yoav/non-coroutine-iterators
Nov 3, 2026
Merged

ericeil merged 2 commits into
mainfrom
yoav/non-coroutine-iterators

Conversation

@yoav-rodeh

@yoav-rodeh yoav-rodeh commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Set and map iteration no longer uses coroutines. HashTreapSet.iterator() was a sequence {} wrapped around a second one (asTreapSequence), and map entries/keys/values started a new coroutine for every node.

  • TreapNodeIterator (in Treap.kt): in-order node walk with an explicit array stack.
  • HashTreapSet/SortedTreapSet.iterator(), map entries/keys/values (through a new internal entryIterator(transform)), entrySequence() and asTreapSequence() all go through it.
  • Iteration order is unchanged, including elements chained under one hash code.
  • zip still uses sequence {}; it didn't show up in profiles.

Why

A JFR profile of a large EVM rule in the Certora Prover (about 1M TAC commands, 359 JVM CPU-s) spent 12.5% of JVM CPU inside coroutine/sequence iteration machinery, spread over about 100 call sites (for loops, any, filter, singleOrNull, groupBy, ... on treap sets and maps).

Results

Micro-benchmark, best of 15 rounds, ns per element, before -> after:

Elements Hash set Sorted set Hash map entries Hash map keys
50 24.7 -> 5.6 12.8 -> 4.3 40.8 -> 4.7 50.7 -> 4.8
2,000 33.4 -> 6.6 17.2 -> 6.5 44.4 -> 6.7 48.5 -> 4.6

At these sizes iterating now costs about the same as forEachElement/forEachEntry. At 100,000 elements memory access dominates and the gain is about 2x.

Prover profile of the same rule with the first commit of this PR patched in: iteration machinery went from 12.5% to 6.0% of JVM CPU, same verdict. 1.5 points of the remaining 6.0% were the ArrayList stack growing, which the second commit removes; the rest is mostly Kotlin sequences in the prover's own code.

Testing

  • All 150 library tests pass (./gradlew :collect:test).
  • New tests (iteratorMatchesForEachElement, iteratorsMatchForEachEntry, run for hashed and sorted sets and maps): the iterators return the same elements in the same order as forEachElement/forEachEntry, with many hash collisions, and next() past the end throws NoSuchElementException.

🤖 Generated with Claude Code

yoav-rodeh and others added 2 commits November 3, 2026 12:11
Set and map iterators (and keys, values, entries, asTreapSequence) were
`sequence {}` builders, nested two deep for hashed sets and with a new
coroutine per node for map entries. A node iterator with an explicit stack
replaces them; iteration order is unchanged.

Best-of-15 ns per element, before -> after, at 50 / 2,000 elements:
hashed set 24.7 -> 6.6 / 33.4 -> 6.0, sorted set 12.8 -> 4.3 / 17.2 -> 6.3,
hashed map entries 40.8 -> 5.8 / 44.4 -> 6.2, keys 50.7 -> 5.3 / 48.5 -> 4.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yoav-rodeh
yoav-rodeh requested a review from ericeil November 3, 2026 12:47

@ericeil ericeil left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks great! I've been meaning to try this for a long time. :)

@ericeil
ericeil merged commit a2f4d1d into main Nov 3, 2026
1 check passed
@ericeil
ericeil deleted the yoav/non-coroutine-iterators branch November 3, 2026 23:46
@ericeil

ericeil commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Released as version 1.7.1

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.

2 participants