gh-138765: Release items cached by itertools.tee once all iterators pass them - #154880
Draft
serhiy-storchaka wants to merge 1 commit into
Draft
gh-138765: Release items cached by itertools.tee once all iterators pass them#154880serhiy-storchaka wants to merge 1 commit into
serhiy-storchaka wants to merge 1 commit into
Conversation
…tors pass them A teedataobject no longer owns the items it caches. They are owned by its direct referrers: the tee objects positioned in the link, and the preceding link, which stands for the tee objects that have not reached it yet. Each referrer drops its reference as it steps over an item, so an item is released as soon as the last tee object passes it, instead of when the whole 57-item link is retired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documentation build overview
|
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.
Draft: the free-threaded build is not correct yet, see below.
Implements the scheme from this comment, following up on the Discourse thread. A
teedataobjectno longer owns its items; its direct referrers do — the tee objects positioned in the link ownitems[index:numread], the preceding link ownsitems[0:numread]on behalf of the branches that have not arrived. Ownership moves rather than being duplicated, andsizeof(teedataobject)is unchanged at 504 bytes (nrefsisPy_ssize_t;numreadandrunningbecomeshortto pay for it).Measured on an
-O3build pinned to one core, best of 4×25×20 runs, items are unique heap objects so every reference count operation is real. A benchmark performing noteeat all calibrates binary-to-binary bias at 1.02x.Iteration is neutral to faster — a branch hands its own reference to the caller instead of taking a new one, so one reference count operation per
next()disappears. The cost is where @pochmann predicted on Discourse: creating a branch inside an already filled link is O(items it can still reach) instead of O(1), so__copy__goes from 23 ns to at most 56 ns, bounded byLINKCELLS. Steady iteration never pays it.One implementation note: the initial references have to be taken with a single
_Py_RefcntAdd(value, nrefs - 1). A loop ofPy_INCREFcosts 12% on the five-branch lockstep benchmark on its own, because the read-modify-writes on one reference count serialize.Status
Default build: the full test suite passes,
test_itertools -R 3:3is clean, and a randomized stress test over branch copies, abandonment and cycles shows no leak.Free-threaded build: not correct yet.
test_free_threading.test_itertools -Faborts with a negative reference count after a few dozen iterations; unpatched main under the same loop only hits the pre-existing flakytest_cycleStopIteration. A debug-only per-item ownership tracer shows an item created with two owners, released by the branch stepping over it, and then released twice more — once by the preceding link, and once by a thread that lost the jump race and released an item its attach never covered. The suspect is therefore the attach/detach pair that a losing thread performs aroundteedataobject_jumplink(): the set of cached items changes between its attach and its detach. I first suspected the read-and-advance step instead and made it atomic underPy_BEGIN_CRITICAL_SECTION2for cached items, which is kept here because it is sound and cheaper, but it is not the fix.One real ordering bug was found on the way and is fixed here:
tee_copy_impl()tracked the copy before taking its references, so a concurrent GC could see reference edges that did not exist yet and collect live items.itertools.teecauses memory leak by not releasing cached items once consumed by all iterators #138765