-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
gh-115999: Add free-threaded specialization for FOR_ITER #128798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
54c551e
Add free-threaded specialization for lists and tuples.
Yhg1s 1433cd3
Add specialization for range iterators and generators, both about as
Yhg1s a662ecf
Add missing ifdef guard.
Yhg1s 2fef94b
Fix copy-paste mistake.
Yhg1s 0870ce7
Regen cases.
Yhg1s cf5fcd0
Rework FOR_ITER specialization in the free-threaded build to only app…
Yhg1s bb495b0
Fix whitespace for linter.
Yhg1s 940b7c9
Incorporate changes from #128445 into the free-threaded branch of
Yhg1s a800d75
Drop redundant assert.
Yhg1s 1781cad
Don't mark _PyList_GetItemRefNoLock as non-escaping.
Yhg1s 358199a
Address reviewer comments, and drop test_iteration_deopt.
Yhg1s 5c16a5e
Merge branch 'main' into for-iter-spec
Yhg1s 07d3033
Regenerate cases after merge.
Yhg1s 4326376
Merge branch 'main' into for-iter-spec
Yhg1s 375399d
Make the free-threaded FOR_ITER work in the tier 2 interpreter/jit.
Yhg1s File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import dis | ||
import queue | ||
import threading | ||
import time | ||
import unittest | ||
from test.support import (import_helper, cpython_only, Py_GIL_DISABLED, | ||
requires_specialization_ft) | ||
|
||
_testinternalcapi = import_helper.import_module("_testinternalcapi") | ||
_testlimitedcapi = import_helper.import_module("_testlimitedcapi") | ||
|
||
NUMTHREADS = 5 | ||
|
||
def get_tlbc_instructions(f): | ||
co = dis._get_code_object(f) | ||
tlbc = _testinternalcapi.get_tlbc(co) | ||
return [i.opname for i in dis._get_instructions_bytes(tlbc)] | ||
|
||
|
||
class IterationDeoptTests(unittest.TestCase): | ||
def check_deopt(self, get_iter, opcode, is_generator=False): | ||
input = range(100) | ||
expected_len = len(input) | ||
q = queue.Queue() | ||
barrier = threading.Barrier(NUMTHREADS + 1) | ||
done = threading.Event() | ||
def worker(): | ||
# A complicated dance to get a weak reference to an iterator | ||
# _only_ (strongly) referenced by the for loop, so that we can | ||
# force our loop to deopt mid-way through. | ||
it = get_iter(input) | ||
ref = _testlimitedcapi.pylong_fromvoidptr(it) | ||
Yhg1s marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
q.put(ref) | ||
# We can't use enumerate without affecting the loop, so keep a | ||
# manual counter. | ||
i = 0 | ||
loop_a_little_more = 5 | ||
results = [] | ||
try: | ||
# Make sure we're not still specialized from a previous run. | ||
ops = get_tlbc_instructions(worker) | ||
self.assertIn('FOR_ITER', ops) | ||
self.assertNotIn(opcode, ops) | ||
for item in it: | ||
results.append(item) | ||
i += 1 | ||
|
||
# We have to be very careful exiting the loop, because | ||
# if the main thread hasn't dereferenced the unsafe | ||
# weakref to our iterator yet, exiting will make it | ||
# invalid and cause a crash. Getting the timing right is | ||
# difficult, though, since it depends on the OS | ||
# scheduler and the system load. As a final safeguard, | ||
# if we're close to finishing the loop, just wait for the | ||
# main thread. | ||
if i + loop_a_little_more > expected_len: | ||
done.wait() | ||
|
||
if i == 1: | ||
del it | ||
# Warm up. The first iteration didn't count because of | ||
# the extra reference to the iterator. | ||
if i < 10: | ||
continue | ||
if i == 10: | ||
ops = get_tlbc_instructions(worker) | ||
self.assertIn(opcode, ops) | ||
# Let the main thread know it's time to reference our iterator. | ||
barrier.wait() | ||
continue | ||
# Continue iterating while at any time our loop may be | ||
# forced to deopt, but try to get the thread scheduler | ||
# to give the main thread a chance to run. | ||
if not done.is_set(): | ||
time.sleep(0) | ||
continue | ||
if loop_a_little_more: | ||
# Loop a little more after 'done' is set to make sure we | ||
# introduce a tsan-detectable race if the loop isn't | ||
# deopting appropriately. | ||
loop_a_little_more -= 1 | ||
continue | ||
break | ||
self.assertEqual(results, list(input)[:i]) | ||
except threading.BrokenBarrierError: | ||
return | ||
except Exception as e: | ||
# In case the exception happened before the last barrier, | ||
# reset it so nothing is left hanging. | ||
barrier.reset() | ||
# In case it's the final assertion that failed, just add it | ||
# to the result queue so it'll show up in the normal test | ||
# output. | ||
q.put(e) | ||
raise | ||
q.put("SUCCESS") | ||
# Reset specialization and thread-local bytecode from previous runs. | ||
worker.__code__ = worker.__code__.replace() | ||
threads = [threading.Thread(target=worker) for i in range(NUMTHREADS)] | ||
for t in threads: | ||
t.start() | ||
# Get the "weakrefs" from the worker threads. | ||
refs = [q.get() for i in range(NUMTHREADS)] | ||
# Wait for each thread to finish its specialization check. | ||
barrier.wait() | ||
# Dereference the "weakrefs" we were sent in an extremely unsafe way. | ||
iterators = [_testlimitedcapi.pylong_asvoidptr(ref) for ref in refs] | ||
done.set() | ||
self.assertNotIn(None, iterators) | ||
# Read data that the iteration writes, to trigger data races if they | ||
# don't deopt appropriately. | ||
if is_generator: | ||
for it in iterators: | ||
it.gi_running | ||
else: | ||
for it in iterators: | ||
it.__reduce__() | ||
for t in threads: | ||
t.join() | ||
results = [q.get() for i in range(NUMTHREADS)] | ||
self.assertEqual(results, ["SUCCESS"] * NUMTHREADS) | ||
|
||
@cpython_only | ||
@requires_specialization_ft | ||
@unittest.skipIf(not Py_GIL_DISABLED, "requires free-threading") | ||
def test_deopt_leaking_iterator_list(self): | ||
def make_list_iter(input): | ||
return iter(list(input)) | ||
self.check_deopt(make_list_iter, 'FOR_ITER_LIST') | ||
|
||
@cpython_only | ||
@requires_specialization_ft | ||
@unittest.skipIf(not Py_GIL_DISABLED, "requires free-threading") | ||
def test_deopt_leaking_iterator_tuple(self): | ||
def make_tuple_iter(input): | ||
return iter(tuple(input)) | ||
self.check_deopt(make_tuple_iter, 'FOR_ITER_TUPLE') | ||
|
||
@cpython_only | ||
@requires_specialization_ft | ||
@unittest.skipIf(not Py_GIL_DISABLED, "requires free-threading") | ||
def test_deopt_leaking_iterator_range(self): | ||
def make_range_iter(input): | ||
return iter(input) | ||
self.check_deopt(make_range_iter, 'FOR_ITER_RANGE') | ||
|
||
@cpython_only | ||
@requires_specialization_ft | ||
@unittest.skipIf(not Py_GIL_DISABLED, "requires free-threading") | ||
def test_deopt_leaking_iterator_generator(self): | ||
def gen(input): | ||
for item in input: | ||
yield item | ||
self.check_deopt(gen, 'FOR_ITER_GEN', is_generator=True) |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.