GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-115999: Add free-threaded specialization for FOR_ITER by Yhg1s · Pull Request #128798 · python/cpython

Conversation

@Yhg1s

@Yhg1s Yhg1s commented Jan 13, 2025 •
edited by bedevere-app Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Add free-threaded versions of existing specialization for FOR_ITER (list, tuples, fast range iterators and generators), without significantly affecting their thread-safety. (Iterating over shared lists/tuples/ranges should be fine like before. Reusing iterators between threads is not fine, like before. Sharing generators between threads is a recipe for significant crashes, like before.)

  • Issue: Make the specializing interpreter thread-safe in --disable-gil builds #115999

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Yhg1s added 2 commits January 13, 2025 01:59
@Yhg1s
Add free-threaded specialization for lists and tuples.
54c551e
@Yhg1s
Add specialization for range iterators and generators, both about as
1433cd3
thread-safe as without spcialization (i.e. not much to none at all).
@bedevere-app bedevere-app Bot mentioned this pull request Jan 13, 2025
Make the specializing interpreter thread-safe in --disable-gil builds #115999
Closed
@Yhg1s
Yhg1s requested review from colesbury and mpage January 13, 2025 19:19
@Yhg1s

Yhg1s commented Jan 13, 2025

Copy link
Copy Markdown
Member Author

I still need to add tests for the specialization (rather than the thread-safety, which are in #128637), but otherwise the PR is good enough to review I think.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Yhg1s added 3 commits January 13, 2025 23:32
@Yhg1s
Add missing ifdef guard.
a662ecf
@Yhg1s
Fix copy-paste mistake.
2fef94b
@Yhg1s
Regen cases.
0870ce7
@mpage

mpage commented Jan 14, 2025

Copy link
Copy Markdown
Contributor

Why not use the approach that you suggested in discord where we only specialize for the case where the iterator is uniquely referenced by the current thread? It seems like that should cover the overwhelmingly common case, would be simpler to implement, and the resulting instructions would be faster.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Yhg1s

Yhg1s commented Jan 14, 2025

Copy link
Copy Markdown
Member Author

I realised that approach wouldn't work for the list referenced by the list iterator (which is the bulk of the work for list iteration) because we're not doing any refcount operations on it (on purpose) and _PyObject_IsUniquelyReferenced() will only do the right thing if all threads hold strong references... and then I think I forgot we should still be able to use that approach for the iterators themselves. I'll see if I can poke holes into that idea tomorrow.

mpage reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

colesbury
colesbury reviewed Jan 14, 2025
View reviewed changes
Comment thread Objects/tupleobject.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

This was referenced Jan 20, 2025
Race in concurrent iteration over range iterators #129068
Closed
Race in concurrent list mutation and item retrieval #129069
Closed
Yhg1s added 2 commits January 21, 2025 13:32
@Yhg1s
Rework FOR_ITER specialization in the free-threaded build to only app…
cf5fcd0
…ly to

uniquely referenced iterators. This handles the common case of 'for item in
seq' (where 'seq' is a list, tuple or range object) and 'for item in
generator_function()', but not, for example, 'g = gen(...); for item in g:'.
@Yhg1s
Fix whitespace for linter.
bb495b0
@Yhg1s

Yhg1s commented Jan 21, 2025

Copy link
Copy Markdown
Member Author

I've added a test that exercises the deopt paths, although for it to pass with ThreadSanitizer relies on #128637 going in first. The test is a little wacky because for list/range/tuple iterators it's not easy to leak the uniquely referenced iterators. (For generators it could just use a weakref.) I wrote the test to make sure the deopt paths were actually safe, but I'm not sure if long-term those tests make sense. We could just keep them around while we fiddle with specialization, but drop them once we're sure the iterators can't leak (or we change the entire approach to iterator specialization.)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Yhg1s
Yhg1s marked this pull request as ready for review January 21, 2025 15:21
@Yhg1s
Yhg1s requested a review from markshannon as a code owner January 21, 2025 15:21
@bedevere-app bedevere-app Bot added the awaiting core review label Jan 21, 2025
Yhg1s added 2 commits January 21, 2025 16:31
@Yhg1s
Incorporate changes from python#128445 into the free-threaded branch of
940b7c9
FOR_ITER_LIST specialization.
@Yhg1s
Drop redundant assert.
a800d75
@mpage
mpage requested a review from colesbury January 23, 2025 18:35
mpage
mpage reviewed Jan 23, 2025
View reviewed changes
Comment thread Python/bytecodes.c Outdated
Comment on lines +3114 to +3115
// For free-threaded Python, the loop exit can happen at any point during item
// retrieval, so separate ops don't make much sense.

@mpage mpage Jan 23, 2025

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

It doesn't seem like this comment applies to the tuple specialization. Can you delete it if not?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Yhg1s Yhg1s Jan 24, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Done.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Python/bytecodes.c Outdated
Comment on lines +3031 to +3032
// For free-threaded Python, the loop exit can happen at any point during item
// retrieval, so separate ops don't make much sense.

@mpage mpage Jan 23, 2025

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Can you expand a little bit on why this doesn't make sense? It'd be nice to keep the structure of the ops the same between the two builds.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Yhg1s Yhg1s Jan 24, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Done.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

colesbury
colesbury reviewed Jan 23, 2025
View reviewed changes
Comment thread Include/internal/pycore_list.h Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Tools/cases_generator/analyzer.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/specialize.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/specialize.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/test/test_free_threading/test_iteration_deopt.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Yhg1s added 2 commits January 24, 2025 14:21
@Yhg1s
Don't mark _PyList_GetItemRefNoLock as non-escaping.
1781cad
@Yhg1s
Address reviewer comments, and drop test_iteration_deopt.
358199a
markshannon
markshannon reviewed Jan 28, 2025
View reviewed changes
Comment thread Include/internal/pycore_opcode_metadata.h
[FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG },
[FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG },
[FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },

@markshannon markshannon Jan 28, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Why does this need the HAS_ESCAPES_FLAG? How does it escape?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Yhg1s Yhg1s Jan 28, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

The mechanism for fetching an item from a shared list includes calling _Py_TryIncrefCompare, which has a path which DECREFs.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Jan 28, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Which path? I'm confused as why an incref would need to decref.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Yhg1s Yhg1s Jan 28, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

When the reference is removed from the array that backs the list between the time it's retrieved and it's returned.

cpython/Include/internal/pycore_object.h

Lines 595 to 610 in 180ee43

/* Tries to incref the object op and ensures that *src still points to it. */
static inline int
_Py_TryIncrefCompare(PyObject **src, PyObject *op)
{
if (_Py_TryIncrefFast(op)) {
return 1;
}
if (!_Py_TryIncRefShared(op)) {
return 0;
}
if (op != _Py_atomic_load_ptr(src)) {
Py_DECREF(op);
return 0;
}
return 1;
}

Added in #114512.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@markshannon markshannon Jan 28, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I don't see how that works. What happens if *src is modified after op != _Py_atomic_load_ptr(src) but before the function returns?

Regardless, we want to keep FOR_ITER_LIST non-escaping in the default build.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@Yhg1s Yhg1s Jan 29, 2025

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

@colesbury do you remember why lists use _Py_TryXGetRef (in list_get_item_ref) or why it matters that the src ptr still refers to the same item? Can the object be invalid in some way that still requires us to DECREF it?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@colesbury colesbury Jan 29, 2025

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

There are two hazards that _Py_TryXGetRef handles:

  1. The object op may be deallocated between the initial load and the incref. The try-incref, in cooperation with the allocator, handles this case. It returns zero if op has zero refcount.
  2. The memory block for op may be deallocated and reallocated for a different object in between the initial load and incref. In that case the try-incref succeeds, but the subsequent check fails and we need to decref op.

It does not matter if *src is modified after op != _Py_atomic_load_ptr(src). op will still be a valid reference to an object that was the next element in the list at some point during the operation. It's always been possible for the list to be concurrently modified between the execution of FOR_ITER and subsequent code that uses the result.

See https://peps.python.org/pep-0703/#optimistically-avoiding-locking

Regardless, we want to keep FOR_ITER_LIST non-escaping in the default build

Is there any performance reason to do so?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@brandtbucher brandtbucher Jan 29, 2025 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Regardless, we want to keep FOR_ITER_LIST non-escaping in the default build

Is there any performance reason to do so?

Just to follow up: this does make JIT code slightly worse for traces covering for loops over lists. Since the _ITER_NEXT_LIST at the top of the loop is now escaping, this will add a new _CHECK_VALIDITY uop to the start of each loop body where none exists currently.

Consider this loop:

def f():
    x = 0
    for i in list(range(10_000)):
        x += i

This change increases the number of validity checks per loop from 2 to 3. Before:

_MAKE_WARM
_SET_IP
_CHECK_PERIODIC
_CHECK_VALIDITY <------------- OLD
_ITER_CHECK_LIST
_GUARD_NOT_EXHAUSTED_LIST
_ITER_NEXT_LIST
_SET_IP
_STORE_FAST_1
_CHECK_VALIDITY <------------- OLD
_LOAD_FAST_0
_LOAD_FAST_1
_GUARD_BOTH_INT
_BINARY_OP_ADD_INT
_SET_IP
_STORE_FAST_0
_JUMP_TO_TOP

After:

_MAKE_WARM
_SET_IP
_CHECK_PERIODIC
_CHECK_VALIDITY <------------- OLD
_ITER_CHECK_LIST
_GUARD_NOT_EXHAUSTED_LIST
_ITER_NEXT_LIST
_CHECK_VALIDITY_AND_SET_IP <-- NEW
_STORE_FAST_1
_CHECK_VALIDITY <------------- OLD
_LOAD_FAST_0
_LOAD_FAST_1
_GUARD_BOTH_INT
_BINARY_OP_ADD_INT
_SET_IP
_STORE_FAST_0
_JUMP_TO_TOP

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

colesbury reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

@markshannon markshannon Mar 7, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Just to clarify:
FOR_ITER_LIST is escaping because _PyList_GetItemRefNoLock is escaping, and _PyList_GetItemRefNoLock is escaping because _Py_TryIncrefCompareStackRef is escaping.

Either we need to find an alternative approach or make _Py_TryIncrefCompareStackRef non-escaping.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Yhg1s added 2 commits February 18, 2025 14:05
@Yhg1s
Merge branch 'main' into for-iter-spec
5c16a5e
@Yhg1s
Regenerate cases after merge.
07d3033
Yhg1s added 2 commits March 5, 2025 15:31
@Yhg1s
Merge branch 'main' into for-iter-spec
4326376
@Yhg1s
Make the free-threaded FOR_ITER work in the tier 2 interpreter/jit.
375399d
@Yhg1s Yhg1s added the skip news label Mar 6, 2025
@Yhg1s

Yhg1s commented Mar 6, 2025

Copy link
Copy Markdown
Member Author

@markshannon This is the FOR_ITER specialization you wanted to take another look at.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

markshannon
markshannon approved these changes Mar 7, 2025
View reviewed changes

@markshannon markshannon left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

It's a shame about FOR_ITER_LIST becoming escaping.

We need to make _Py_TryIncrefCompareStackRef non-escaping, but not in this PR.
Otherwise we'll end up with everything escaping, as more code uses _Py_TryIncrefCompareStackRef.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Python/bytecodes.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/specialize.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Include/internal/pycore_opcode_metadata.h
[FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG },
[FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG },
[FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },

@markshannon markshannon Mar 7, 2025

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Just to clarify:
FOR_ITER_LIST is escaping because _PyList_GetItemRefNoLock is escaping, and _PyList_GetItemRefNoLock is escaping because _Py_TryIncrefCompareStackRef is escaping.

Either we need to find an alternative approach or make _Py_TryIncrefCompareStackRef non-escaping.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting core review labels Mar 7, 2025
@Yhg1s

Yhg1s commented Mar 8, 2025

Copy link
Copy Markdown
Member Author

We need to make _Py_TryIncrefCompareStackRef non-escaping, but not in this PR. Otherwise we'll end up with everything escaping, as more code uses _Py_TryIncrefCompareStackRef.

I think we discussed this in the weekly meeting a month or two ago... I think the only way to avoid it is to delay DECREF'ing objects we've accidentally incorrectly INCREF'ed -- that is to say, objects that were newly allocated from the same memory as the object we were trying to INCREF but we lost the INCREF/DECREF race with another thread on. I think in this case that might not be too bad: if the INCREF fails we deopt anyway, and we could delay that DECREF until that deopt. For other cases where we need to TryIncref, I'm not sure of the impact of delaying (plus, we'd need some mechanism for the delay).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Yhg1s

Yhg1s commented Mar 10, 2025

Copy link
Copy Markdown
Member Author

I think the only way to avoid it is to delay DECREF'ing objects we've accidentally incorrectly INCREF'ed -- that is to say, objects that were newly allocated from the same memory as the object we were trying to INCREF but we lost the INCREF/DECREF race with another thread on. I think in this case that might not be too bad: if the INCREF fails we deopt anyway, and we could delay that DECREF until that deopt.

@markshannon let me know if you want that to happen in this PR (but I'd prefer it not).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@markshannon

markshannon commented Mar 11, 2025

Copy link
Copy Markdown
Member

No need to do it in this PR.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Yhg1s
Yhg1s merged commit de2f7da into python:main Mar 12, 2025
@bedevere-app bedevere-app Bot removed the awaiting merge label Mar 12, 2025
plashchynski pushed a commit to plashchynski/cpython that referenced this pull request Mar 17, 2025
@Yhg1s @plashchynski
pythongh-115999: Add free-threaded specialization for FOR_ITER (pytho…
f669fb4
…n#128798)

Add free-threaded versions of existing specialization for FOR_ITER (list, tuples, fast range iterators and generators), without significantly affecting their thread-safety. (Iterating over shared lists/tuples/ranges should be fine like before. Reusing iterators between threads is not fine, like before. Sharing generators between threads is a recipe for significant crashes, like before.)
@Yhg1s
Yhg1s deleted the for-iter-spec branch August 13, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mpage mpage mpage left review comments
@colesbury colesbury colesbury left review comments
@brandtbucher brandtbucher brandtbucher left review comments
@markshannon markshannon markshannon approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

skip news

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@Yhg1s @mpage @markshannon @colesbury @brandtbucher
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.