summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorNeil Schemenauer <nas-github@arctrix.com>2024-12-19 18:21:17 (GMT)
committerGitHub <noreply@github.com>2024-12-19 18:21:17 (GMT)
commit1b15c89a17ca3de6b05de5379b8717e9738c51ef (patch)
tree00a4ea63dec7879e81d2ea3d75f43cb8a03ee1b5 /Lib
parentd2f1d917e8b3d2dd8f35495c7632a32688883332 (diff)
downloadcpython-1b15c89a17ca3de6b05de5379b8717e9738c51ef.zip
cpython-1b15c89a17ca3de6b05de5379b8717e9738c51ef.tar.gz
cpython-1b15c89a17ca3de6b05de5379b8717e9738c51ef.tar.bz2
gh-115999: Specialize `STORE_ATTR` in free-threaded builds. (gh-127838)
* Add `_PyDictKeys_StringLookupSplit` which does locking on dict keys and use in place of `_PyDictKeys_StringLookup`. * Change `_PyObject_TryGetInstanceAttribute` to use that function in the case of split keys. * Add `unicodekeys_lookup_split` helper which allows code sharing between `_Py_dict_lookup` and `_PyDictKeys_StringLookupSplit`. * Fix locking for `STORE_ATTR_INSTANCE_VALUE`. Create `_GUARD_TYPE_VERSION_AND_LOCK` uop so that object stays locked and `tp_version_tag` cannot change. * Pass `tp_version_tag` to `specialize_dict_access()`, ensuring the version we store on the cache is the correct one (in case of it changing during the specalize analysis). * Split `analyze_descriptor` into `analyze_descriptor_load` and `analyze_descriptor_store` since those don't share much logic. Add `descriptor_is_class` helper function. * In `specialize_dict_access`, double check `_PyObject_GetManagedDict()` in case we race and dict was materialized before the lock. * Avoid borrowed references in `_Py_Specialize_StoreAttr()`. * Use `specialize()` and `unspecialize()` helpers. * Add unit tests to ensure specializing happens as expected in FT builds. * Add unit tests to attempt to trigger data races (useful for running under TSAN). * Add `has_split_table` function to `_testinternalcapi`.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/test/test_free_threading/test_races.py141
-rw-r--r--Lib/test/test_opcache.py66
2 files changed, 207 insertions, 0 deletions
diff --git a/Lib/test/test_free_threading/test_races.py b/Lib/test/test_free_threading/test_races.py
index 09e1d52..6998255 100644
--- a/Lib/test/test_free_threading/test_races.py
+++ b/Lib/test/test_free_threading/test_races.py
@@ -4,6 +4,7 @@ import functools
import threading
import time
import unittest
+import _testinternalcapi
from test.support import threading_helper
@@ -129,6 +130,146 @@ class TestRaces(TestBase):
# with the cell binding being changed).
do_race(access, mutate)
+ def test_racing_to_bool(self):
+
+ seq = [1]
+
+ class C:
+ def __bool__(self):
+ return False
+
+ def access():
+ if seq:
+ return 1
+ else:
+ return 2
+
+ def mutate():
+ nonlocal seq
+ seq = [1]
+ time.sleep(0)
+ seq = C()
+ time.sleep(0)
+
+ do_race(access, mutate)
+
+ def test_racing_store_attr_slot(self):
+ class C:
+ __slots__ = ['x', '__dict__']
+
+ c = C()
+
+ def set_slot():
+ for i in range(10):
+ c.x = i
+ time.sleep(0)
+
+ def change_type():
+ def set_x(self, x):
+ pass
+
+ def get_x(self):
+ pass
+
+ C.x = property(get_x, set_x)
+ time.sleep(0)
+ del C.x
+ time.sleep(0)
+
+ do_race(set_slot, change_type)
+
+ def set_getattribute():
+ C.__getattribute__ = lambda self, x: x
+ time.sleep(0)
+ del C.__getattribute__
+ time.sleep(0)
+
+ do_race(set_slot, set_getattribute)
+
+ def test_racing_store_attr_instance_value(self):
+ class C:
+ pass
+
+ c = C()
+
+ def set_value():
+ for i in range(100):
+ c.x = i
+
+ set_value()
+
+ def read():
+ x = c.x
+
+ def mutate():
+ # Adding a property for 'x' should unspecialize it.
+ C.x = property(lambda self: None, lambda self, x: None)
+ time.sleep(0)
+ del C.x
+ time.sleep(0)
+
+ do_race(read, mutate)
+
+ def test_racing_store_attr_with_hint(self):
+ class C:
+ pass
+
+ c = C()
+ for i in range(29):
+ setattr(c, f"_{i}", None)
+
+ def set_value():
+ for i in range(100):
+ c.x = i
+
+ set_value()
+
+ def read():
+ x = c.x
+
+ def mutate():
+ # Adding a property for 'x' should unspecialize it.
+ C.x = property(lambda self: None, lambda self, x: None)
+ time.sleep(0)
+ del C.x
+ time.sleep(0)
+
+ do_race(read, mutate)
+
+ def make_shared_key_dict(self):
+ class C:
+ pass
+
+ a = C()
+ a.x = 1
+ return a.__dict__
+
+ def test_racing_store_attr_dict(self):
+ """Test STORE_ATTR with various dictionary types."""
+ class C:
+ pass
+
+ c = C()
+
+ def set_value():
+ for i in range(20):
+ c.x = i
+
+ def mutate():
+ nonlocal c
+ c.x = 1
+ self.assertTrue(_testinternalcapi.has_inline_values(c))
+ for i in range(30):
+ setattr(c, f"_{i}", None)
+ self.assertFalse(_testinternalcapi.has_inline_values(c.__dict__))
+ c.__dict__ = self.make_shared_key_dict()
+ self.assertTrue(_testinternalcapi.has_split_table(c.__dict__))
+ c.__dict__[1] = None
+ self.assertFalse(_testinternalcapi.has_split_table(c.__dict__))
+ c = C()
+
+ do_race(set_value, mutate)
+
if __name__ == "__main__":
unittest.main()
diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py
index 94709e2..ad0b0c4 100644
--- a/Lib/test/test_opcache.py
+++ b/Lib/test/test_opcache.py
@@ -1385,6 +1385,72 @@ class TestSpecializer(TestBase):
@cpython_only
@requires_specialization_ft
+ def test_store_attr_slot(self):
+ class C:
+ __slots__ = ['x']
+
+ def set_slot():
+ c = C()
+ for i in range(100):
+ c.x = i
+
+ set_slot()
+
+ self.assert_specialized(set_slot, "STORE_ATTR_SLOT")
+ self.assert_no_opcode(set_slot, "STORE_ATTR")
+
+ # Adding a property for 'x' should unspecialize it.
+ C.x = property(lambda self: None, lambda self, x: None)
+ set_slot()
+ self.assert_no_opcode(set_slot, "STORE_ATTR_SLOT")
+
+ @cpython_only
+ @requires_specialization_ft
+ def test_store_attr_instance_value(self):
+ class C:
+ pass
+
+ def set_value():
+ c = C()
+ for i in range(100):
+ c.x = i
+
+ set_value()
+
+ self.assert_specialized(set_value, "STORE_ATTR_INSTANCE_VALUE")
+ self.assert_no_opcode(set_value, "STORE_ATTR")
+
+ # Adding a property for 'x' should unspecialize it.
+ C.x = property(lambda self: None, lambda self, x: None)
+ set_value()
+ self.assert_no_opcode(set_value, "STORE_ATTR_INSTANCE_VALUE")
+
+ @cpython_only
+ @requires_specialization_ft
+ def test_store_attr_with_hint(self):
+ class C:
+ pass
+
+ c = C()
+ for i in range(29):
+ setattr(c, f"_{i}", None)
+
+ def set_value():
+ for i in range(100):
+ c.x = i
+
+ set_value()
+
+ self.assert_specialized(set_value, "STORE_ATTR_WITH_HINT")
+ self.assert_no_opcode(set_value, "STORE_ATTR")
+
+ # Adding a property for 'x' should unspecialize it.
+ C.x = property(lambda self: None, lambda self, x: None)
+ set_value()
+ self.assert_no_opcode(set_value, "STORE_ATTR_WITH_HINT")
+
+ @cpython_only
+ @requires_specialization_ft
def test_to_bool(self):
def to_bool_bool():
true_cnt, false_cnt = 0, 0