Skip to content

Straight paths for Advance, NextElementBytes and AdvanceIter - #8

Merged
asp24 merged 7 commits into
masterfrom
iter-frontend-2
Oct 10, 2026
Merged

asp24 merged 7 commits into
masterfrom
iter-frontend-2

Conversation

@asp24

@asp24 asp24 commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Applies the approach of #6 to the other three functions that walk the tape one element at a time: Advance, NextElementBytes and AdvanceIter. Each now handles the common case in one straight run. NOPs, the end of the tape and every error go to the code the function had before, which runs from the start, so those cases behave as they did.

Commits

  1. Keep the object walk's stack between iterations. IterRead/*/objects regrew its stack on every call, up to 6 MB per operation on canada. The rows mostly measured the garbage collector, with every Iter copied through a write barrier while it marked. The same binary moved by tens of percent between runs, and the first measurement of this change showed a regression that was not there. The stack is now kept between calls and allocates nothing: citm_catalog 365 → 234 µs, canada 1943 → 979 µs.

  2. Benchmark reading through AdvanceIter. ForEach and Array's helpers read through it, and no benchmark called it. New rows: IterRead/*/iters.

  3. Stop NextElementBytes on two corrupt tapes, found by the differential test.

    • A NOP of zero sent it back to the same entry by recursion until the stack ran out. That is a fatal error, which a caller cannot recover from.
    • A container ending before its start, which Deserialize accepts when the stored distance is zero, moved the object back.
    • Both are now errors, as they already were in AdvanceIter. The parser never produces either.
  4. Advance: one straight path, as AdvanceInto has.

  5. NextElementBytes: one straight path, with no switch on either tag.

  6. AdvanceIter: one straight path. It also stops reading back what it has just written: *dst = *i read off and addNext in one 16-byte load right after the two 8-byte stores that wrote them. The store queue cannot forward that, so the load waited for both stores to reach the cache. That one load took half of the function's time.

  7. README. The list of differences from upstream gains reading. Against v0.4.5, measured side by side with BenchmarkIterRead added to its tree, all 28 rows are faster, geomean -31.7%: walk -30% to -39%, typed -25% to -31%, objects -12% to -22%, iters -43% to -51%.

Time, each commit against its parent

Measured with benchmarks/perfgate/ab.sh: four code layouts a side, six rounds, on twelve cores.

commit rows change other IterRead rows
Advance objects -3.1% to -13.2%, every document within the control
NextElementBytes objects -4.5% to -13.4%; canada, almost no keys, -0.1% within the control
AdvanceIter iters -43.6% to -50.5%, every document within the control

The read group from commit 3 to the end: geomean -13.0% (control +2.1%). ParseNDReuse and the payloads are within the control.

Testing

advance_diff_test.go keeps the previous code of each function as a reference and walks both in lockstep. Walks start from every tag on:

  • the corpus;
  • the corpus with deletions, which leave NOPs;
  • the corpus cut short;
  • the corpus with container ends pointing backwards or past the tape;
  • hand-made edge tapes.

Every field of the Iter, dst and the Object is compared, along with the result, the error and any panic. Seeding faults into the new code checked the test: every one was caught, except one that is equivalent by construction (end < len for end <= len, which only sends the edge case down the slow path).

🤖 Generated with Claude Code

walkObjects pushes every container it has yet to visit, not one per level,
and it grew that stack from 64 entries again on every call: up to 6 MB per
operation on canada. The objects rows measured the garbage collector as much
as the walk - with every Iter copied through a write barrier while it marked -
and moved by tens of percent between runs of the same binary. Kept between
iterations, the stack allocates nothing: citm_catalog 365 -> 234 us, canada
1943 -> 979 us.
asp24 added 5 commits October 10, 2026 20:45
ForEach and Array's helpers hand out every element as an Iter of its own,
through AdvanceIter, and no benchmark called it. The iters rows descend each
document that way.
A NOP of zero sent it back to the same entry by recursion until the stack
ran out - a fatal error, not a panic a caller can recover from. A container
whose end lies before its start - which Deserialize lets through when the
stored distance is zero - gave an element ending before it starts, and moved
the object back. Both are now errors, as they already were in AdvanceIter:
the NOP skip is a loop that refuses a skip of zero, and the element size is
checked before it is used.

Neither comes from the parser: its NOPs skip at least one entry, and its
containers end after they start.
Advance gets the shape AdvanceInto got in 404b6b1. The common case - an
element inside the tape that is not a NOP - is one straight run, and
whether a value word follows is a bit test, not calcNext's switch. A
container is the one case with a second branch, to work out how far to
skip, and the check that the skip does not go backwards stays on that
branch. NOPs and the end of the tape go to the loop after it, unchanged.

Against the parent, four code layouts a side, six rounds on twelve cores:

  IterRead objects  -3.1% to -13.2%, on every document
  walk, typed, iters  within the control

advance_diff_test.go keeps the old Advance as a reference and walks both in
lockstep, from every tag on the corpus, on the corpus cut short, and on it
with container ends pointing backwards or past the tape.
NextElementBytes switched on the key's tag, and then twice on the value's,
through calcNext. The common case - a key whose name lies within its area,
then an element that ends within the tape - is now one straight run: a
bit test for a value word, and a branch taken only for a container. The end
of the object, NOPs and every error go to the code it had before, now
nextElementBytes, from the start, so they behave as they did.

Against the parent, four code layouts a side, six rounds on twelve cores:

  IterRead objects  -4.5% to -13.4%; canada, with almost no keys, -0.1%
  walk, typed, iters  within the control
Half of AdvanceIter's time went to one load. It set i's fields and then
copied *i into dst, and the copy read off and addNext in one 16-byte load
right behind the two 8-byte stores that wrote them. The store queue cannot
forward one load from two stores, so it waited for both to reach the
cache. The old code did the same through calcNext. dst now gets the tape
copied from i and its other fields from registers.

The common case is also one straight run now, as in Advance, and NOPs, the
end of the tape and every error go to the code it had before, now
advanceIter, from the start.

Against the parent, four code layouts a side, six rounds on twelve cores:

  IterRead iters    -43.6% to -50.5%, on every document: -14% to -17%
                    from the straight run, measured alone, and the rest
                    from not reading back
  walk, typed, objects  within the control

The read group as a whole moves -13.0% in geomean; ParseNDReuse and the
payloads within the control.
The list of what differs from upstream named parsing and nothing on the
reading side. Measured against v0.4.5 side by side - BenchmarkIterRead
added to its tree, four code layouts a side, six rounds on twelve cores -
every one of the 28 rows is faster, geomean -31.7%:

  walk     -29.7% to -39.1%
  typed    -24.6% to -30.7%
  objects  -12.5% to -22.5%
  iters    -43.0% to -51.1%

The NextElementBytes fix for corrupt tapes joins the list of fixed inputs.
@asp24
asp24 merged commit b113c35 into master Oct 10, 2026
11 checks passed
@asp24
asp24 deleted the iter-frontend-2 branch October 10, 2026 21:13
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