Skip to content

gh-138765: Release items cached by itertools.tee once all iterators pass them - #154880

Draft
serhiy-storchaka wants to merge 1 commit into
python:mainfrom
serhiy-storchaka:tee-referrer-count
Draft

gh-138765: Release items cached by itertools.tee once all iterators pass them#154880
serhiy-storchaka wants to merge 1 commit into
python:mainfrom
serhiy-storchaka:tee-referrer-count

Conversation

@serhiy-storchaka

Copy link
Copy Markdown
Member

Draft: the free-threaded build is not correct yet, see below.

Implements the scheme from this comment, following up on the Discourse thread. A teedataobject no longer owns its items; its direct referrers do — the tee objects positioned in the link own items[index:numread], the preceding link owns items[0:numread] on behalf of the branches that have not arrived. Ownership moves rather than being duplicated, and sizeof(teedataobject) is unchanged at 504 bytes (nrefs is Py_ssize_t; numread and running become short to pay for it).

Measured on an -O3 build 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 no tee at all calibrates binary-to-binary bias at 1.02x.

benchmark main, ms this PR
two branches, sequential / lockstep 0.376 / 0.421 1.00x / 0.90x
lookahead recipe 0.400 0.96x
five branches, sequential / lockstep 0.641 / 0.685 0.97x / 0.98x
many short tees 2.255 0.98x
copy inside a full link 0.485 1.23x

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 by LINKCELLS. 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 of Py_INCREF costs 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:3 is 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 -F aborts with a negative reference count after a few dozen iterations; unpatched main under the same loop only hits the pre-existing flaky test_cycle StopIteration. 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 around teedataobject_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 under Py_BEGIN_CRITICAL_SECTION2 for 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.

…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>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #33816967 | 📁 Comparing 6a3a1c6 against main (9ccd5bb)

  🔍 Preview build  

2 files changed
± library/itertools.html
± whatsnew/changelog.html

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