Recover scheduler throughput lost to the snapshot-staleness check - #754
Open
wbarnha wants to merge 1 commit into
Open
Recover scheduler throughput lost to the snapshot-staleness check#754wbarnha wants to merge 1 commit into
wbarnha wants to merge 1 commit into
Conversation
Fixing the record-loss bug in #751 cost most of the scheduler's speedup: the length-only shortcut it replaced was unsound, but the identity check that replaced it walks the mapping with .items(), allocating a view, an iterator and a tuple per entry. A single-partition topic reaches a pass boundary on every record, so that walk lands squarely on the hot path and the accelerator fell from 5.7-7.7x over pure Python to 2.2-2.8x. Two changes get it back without weakening the guarantee. TopicBuffer._buffers becomes a plain dict. It was an OrderedDict, with a comment noting it was "a regular dict, but ordered on Python 3.6" -- dicts have been insertion-ordered by language guarantee since 3.7 and Faust requires 3.10, so the subclass bought nothing. It also cost something: OrderedDict is not an exact dict, so it cannot be walked with PyDict_Next. Both _snapshot_is_current() methods then use PyDict_Next when the mapping is an exact dict, walking it with no allocation at all and comparing borrowed references by identity. Anything that is not an exact dict still goes through .items(), so a custom mapping handed to records_iterator keeps working. topology python cython before after 1t x 1p x 500 525.8n 90.2n 2.23x 5.83x 1t x 8p x 200 422.5n 78.2n -- 5.40x 4t x 8p x 100 336.6n 64.3n 2.63x 5.23x 8t x 16p x 50 324.8n 69.3n 2.77x 4.68x The correctness properties from #751 are unchanged and re-verified: both mid-iteration replacement scenarios still match the pure-Python iterator, and the randomised differential test is at 4000/4000. Adds tests for the non-dict mapping fallback and for _buffers being an exact dict, since that is now load-bearing rather than incidental. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019oX4oGCQGgvPJHabjwfaBk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixing the record-loss bug reported in review of #751 cost most of the scheduler's speedup. The length-only shortcut it replaced was unsound — it could not tell "unchanged" from "swapped" — but the identity check that replaced it walks the mapping with
.items(), allocating a view, an iterator and a tuple per entry. A single-partition topic reaches a pass boundary on every record, so that walk lands squarely on the hot path, and the accelerator fell from 5.7–7.7x over pure Python to 2.2–2.8x.Two changes get it back without weakening the guarantee.
TopicBuffer._buffersbecomes a plain dictIt was an
OrderedDict, carrying this comment:Dicts have been insertion-ordered by language guarantee since 3.7, and Faust requires 3.10, so the subclass bought nothing. It also cost something:
OrderedDictis not an exact dict, so it cannot be walked withPyDict_Next.Both
_snapshot_is_current()methods usePyDict_NextWhen the mapping is an exact dict, the check walks it with no allocation at all, comparing borrowed references by identity. Anything that is not an exact dict still goes through
.items(), so a custom mapping handed torecords_iteratorkeeps working.Benchmarks
ns per record, CPython 3.11:
Back to roughly where the unsound version was, with the correctness guarantee kept.
Correctness
Unchanged from #751 and re-verified rather than assumed:
TopicBufferswapped in under an existing topic name, and an iterator swapped in under an existingTP) still match the pure-Python iterator exactly.NO_CYTHON=1.New tests cover the two things this PR makes load-bearing:
.items()fallback;_buffersactually being an exactdict, sincePyDict_Nextsilently would not apply otherwise and the speedup would quietly disappear.Note on the
OrderedDictchangeThis is a small behavioural surface worth a reviewer's eye: anything relying on
_buffersbeing anOrderedDictspecifically —move_to_end,popitem(last=False), equality being order-sensitive — would be affected. Nothing in the tree does;_buffersis only assigned, popped and iterated. Flagging it because it is the one change here that is not purely internal to the.pyx.🤖 Generated with Claude Code
https://claude.ai/code/session_019oX4oGCQGgvPJHabjwfaBk
Generated by Claude Code