summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authormpage <mpage@meta.com>2024-12-03 19:20:20 (GMT)
committerGitHub <noreply@github.com>2024-12-03 19:20:20 (GMT)
commitdabcecfd6dadb9430733105ba36925b290343d31 (patch)
tree56bd2af5b31dbc182c6cd7c81a33745112871391 /Lib
parentfc5a0dc22483a35068888e828c65796d7a792c14 (diff)
downloadcpython-dabcecfd6dadb9430733105ba36925b290343d31.zip
cpython-dabcecfd6dadb9430733105ba36925b290343d31.tar.gz
cpython-dabcecfd6dadb9430733105ba36925b290343d31.tar.bz2
gh-115999: Enable specialization of `CALL` instructions in free-threaded builds (#127123)
The CALL family of instructions were mostly thread-safe already and only required a small number of changes, which are documented below. A few changes were needed to make CALL_ALLOC_AND_ENTER_INIT thread-safe: Added _PyType_LookupRefAndVersion, which returns the type version corresponding to the returned ref. Added _PyType_CacheInitForSpecialization, which takes an init method and the corresponding type version and only populates the specialization cache if the current type version matches the supplied version. This prevents potentially caching a stale value in free-threaded builds if we race with an update to __init__. Only cache __init__ functions that are deferred in free-threaded builds. This ensures that the reference to __init__ that is stored in the specialization cache is valid if the type version guard in _CHECK_AND_ALLOCATE_OBJECT passes. Fix a bug in _CREATE_INIT_FRAME where the frame is pushed to the stack on failure. A few other miscellaneous changes were also needed: Use {LOCK,UNLOCK}_OBJECT in LIST_APPEND. This ensures that the list's per-object lock is held while we are appending to it. Add missing co_tlbc for _Py_InitCleanup. Stop/start the world around setting the eval frame hook. This allows us to read interp->eval_frame non-atomically and preserves the behavior of _CHECK_PEP_523 documented below.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_monitoring.py17
-rw-r--r--Lib/test/test_opcache.py32
-rw-r--r--Lib/test/test_type_cache.py9
3 files changed, 44 insertions, 14 deletions
diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py
index b640aa0..5a4bceb 100644
--- a/Lib/test/test_monitoring.py
+++ b/Lib/test/test_monitoring.py
@@ -11,7 +11,7 @@ import types
import unittest
import test.support
-from test.support import requires_specialization, script_helper
+from test.support import requires_specialization_ft, script_helper
from test.support.import_helper import import_module
_testcapi = test.support.import_helper.import_module("_testcapi")
@@ -850,6 +850,13 @@ class ReturnRecorder:
def __call__(self, code, offset, val):
self.events.append(("return", code.co_name, val))
+# gh-127274: CALL_ALLOC_AND_ENTER_INIT will only cache __init__ methods that
+# are deferred. We only defer functions defined at the top-level.
+class ValueErrorRaiser:
+ def __init__(self):
+ raise ValueError()
+
+
class ExceptionMonitoringTest(CheckEvents):
exception_recorders = (
@@ -1045,16 +1052,12 @@ class ExceptionMonitoringTest(CheckEvents):
)
self.assertEqual(events[0], ("throw", IndexError))
- @requires_specialization
+ @requires_specialization_ft
def test_no_unwind_for_shim_frame(self):
- class B:
- def __init__(self):
- raise ValueError()
-
def f():
try:
- return B()
+ return ValueErrorRaiser()
except ValueError:
pass
diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py
index b7a1813..50b5f36 100644
--- a/Lib/test/test_opcache.py
+++ b/Lib/test/test_opcache.py
@@ -493,6 +493,18 @@ class TestLoadMethodCache(unittest.TestCase):
self.assertFalse(f())
+# gh-127274: CALL_ALLOC_AND_ENTER_INIT will only cache __init__ methods that
+# are deferred. We only defer functions defined at the top-level.
+class MyClass:
+ def __init__(self):
+ pass
+
+
+class InitTakesArg:
+ def __init__(self, arg):
+ self.arg = arg
+
+
class TestCallCache(TestBase):
def test_too_many_defaults_0(self):
def f():
@@ -522,12 +534,8 @@ class TestCallCache(TestBase):
f()
@disabling_optimizer
- @requires_specialization
+ @requires_specialization_ft
def test_assign_init_code(self):
- class MyClass:
- def __init__(self):
- pass
-
def instantiate():
return MyClass()
@@ -544,6 +552,20 @@ class TestCallCache(TestBase):
MyClass.__init__.__code__ = count_args.__code__
instantiate()
+ @disabling_optimizer
+ @requires_specialization_ft
+ def test_push_init_frame_fails(self):
+ def instantiate():
+ return InitTakesArg()
+
+ for _ in range(2):
+ with self.assertRaises(TypeError):
+ instantiate()
+ self.assert_specialized(instantiate, "CALL_ALLOC_AND_ENTER_INIT")
+
+ with self.assertRaises(TypeError):
+ instantiate()
+
@threading_helper.requires_working_threading()
class TestRacesDoNotCrash(TestBase):
diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py
index 66abe73..e109a65 100644
--- a/Lib/test/test_type_cache.py
+++ b/Lib/test/test_type_cache.py
@@ -2,7 +2,7 @@
import unittest
import dis
from test import support
-from test.support import import_helper, requires_specialization
+from test.support import import_helper, requires_specialization, requires_specialization_ft
try:
from sys import _clear_type_cache
except ImportError:
@@ -110,7 +110,6 @@ class TypeCacheTests(unittest.TestCase):
HolderSub.value
@support.cpython_only
-@requires_specialization
class TypeCacheWithSpecializationTests(unittest.TestCase):
def tearDown(self):
_clear_type_cache()
@@ -140,6 +139,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
else:
self.assertIn(opname, self._all_opnames(func))
+ @requires_specialization
def test_class_load_attr_specialization_user_type(self):
class A:
def foo(self):
@@ -160,6 +160,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
self._check_specialization(load_foo_2, A, "LOAD_ATTR", should_specialize=False)
+ @requires_specialization
def test_class_load_attr_specialization_static_type(self):
self.assertNotEqual(type_get_version(str), 0)
self.assertNotEqual(type_get_version(bytes), 0)
@@ -171,6 +172,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
self.assertEqual(get_capitalize_1(str)('hello'), 'Hello')
self.assertEqual(get_capitalize_1(bytes)(b'hello'), b'Hello')
+ @requires_specialization
def test_property_load_attr_specialization_user_type(self):
class G:
@property
@@ -192,6 +194,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
self._check_specialization(load_x_2, G(), "LOAD_ATTR", should_specialize=False)
+ @requires_specialization
def test_store_attr_specialization_user_type(self):
class B:
__slots__ = ("bar",)
@@ -211,6 +214,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
self._check_specialization(store_bar_2, B(), "STORE_ATTR", should_specialize=False)
+ @requires_specialization_ft
def test_class_call_specialization_user_type(self):
class F:
def __init__(self):
@@ -231,6 +235,7 @@ class TypeCacheWithSpecializationTests(unittest.TestCase):
self._check_specialization(call_class_2, F, "CALL", should_specialize=False)
+ @requires_specialization
def test_to_bool_specialization_user_type(self):
class H:
pass