Skip to content

Commit 96ae298

Browse files
committed
gh-155962: Specialize LOAD_ATTR after replacing an instance's __dict__
1 parent f40043e commit 96ae298

2 files changed

Lines changed: 106 additions & 9 deletions

File tree

Lib/test/test_opcache.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,75 @@ def f(self):
6464

6565

6666
class TestLoadAttrCache(unittest.TestCase):
67+
@requires_specialization
68+
def test_load_attr_replaced_dict_specializes(self):
69+
class C:
70+
class_value = "class"
71+
72+
def method(self):
73+
return "class method"
74+
75+
c = C()
76+
c.__dict__ = {
77+
"instance_value": 42,
78+
"class_value": "instance",
79+
"method": "instance method",
80+
}
81+
82+
def get_values():
83+
return c.instance_value, c.class_value, c.method
84+
85+
expected = (42, "instance", "instance method")
86+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
87+
self.assertEqual(get_values(), expected)
88+
89+
opnames = [
90+
instruction.opname
91+
for instruction in dis.get_instructions(get_values, adaptive=True)
92+
]
93+
self.assertEqual(opnames.count("LOAD_ATTR_WITH_HINT"), 3)
94+
95+
@requires_specialization
96+
def test_load_attr_replaced_general_dict_does_not_specialize(self):
97+
class C:
98+
pass
99+
100+
c = C()
101+
c.__dict__ = {"x": 42, 1: None}
102+
103+
def get_x():
104+
return c.x
105+
106+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
107+
self.assertEqual(get_x(), 42)
108+
109+
opnames = {
110+
instruction.opname
111+
for instruction in dis.get_instructions(get_x, adaptive=True)
112+
}
113+
self.assertNotIn("LOAD_ATTR_WITH_HINT", opnames)
114+
115+
@requires_specialization
116+
def test_load_attr_replaced_dict_ignores_stale_shared_key(self):
117+
class C:
118+
x = "class"
119+
120+
c = C()
121+
c.x = "instance"
122+
c.__dict__ = {}
123+
124+
def get_x():
125+
return c.x
126+
127+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
128+
self.assertEqual(get_x(), "class")
129+
130+
opnames = {
131+
instruction.opname
132+
for instruction in dis.get_instructions(get_x, adaptive=True)
133+
}
134+
self.assertNotIn("LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES", opnames)
135+
67136
def test_descriptor_added_after_optimization(self):
68137
class Descriptor:
69138
pass
@@ -1086,6 +1155,30 @@ def write(items):
10861155
opname = "LOAD_ATTR_WITH_HINT"
10871156
self.assert_races_do_not_crash(opname, get_items, read, write)
10881157

1158+
@requires_specialization
1159+
def test_load_attr_with_hint_replaced_dict(self):
1160+
def get_items():
1161+
class C:
1162+
pass
1163+
1164+
items = []
1165+
for _ in range(self.ITEMS):
1166+
item = C()
1167+
item.__dict__ = {"a": None}
1168+
items.append(item)
1169+
return items
1170+
1171+
def read(items):
1172+
for item in items:
1173+
item.a
1174+
1175+
def write(items):
1176+
for item in items:
1177+
item.__dict__ = {"padding": None, "a": None}
1178+
1179+
opname = "LOAD_ATTR_WITH_HINT"
1180+
self.assert_races_do_not_crash(opname, get_items, read, write)
1181+
10891182
@requires_specialization
10901183
def test_load_global_module(self):
10911184
if not have_dict_key_versions():

Python/specialize.c

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ specialize_dict_access_hint(
677677
SPECIALIZATION_FAIL(base_op, SPEC_FAIL_ATTR_SPLIT_DICT);
678678
return 0;
679679
}
680+
if (dict->ma_keys->dk_kind != DICT_KEYS_UNICODE) {
681+
SPECIALIZATION_FAIL(base_op, SPEC_FAIL_ATTR_NON_STRING);
682+
return 0;
683+
}
680684
Py_ssize_t index = _PyDict_LookupIndex(dict, name);
681685
if (index != (uint16_t)index) {
682686
SPECIALIZATION_FAIL(base_op,
@@ -750,10 +754,9 @@ specialize_attr_loadclassattr(PyObject *owner, _Py_CODEUNIT *instr,
750754
uint32_t shared_keys_version);
751755
static int specialize_class_load_attr(PyObject* owner, _Py_CODEUNIT* instr, PyObject* name);
752756

753-
/* Returns true if instances of obj's class are
754-
* likely to have `name` in their __dict__.
755-
* For objects with inline values, we check in the shared keys.
756-
* For other objects, we check their actual dictionary.
757+
/* Returns true if obj is likely to have `name` in its __dict__.
758+
* For objects with valid inline values, we check in the shared keys.
759+
* Otherwise, we check their actual dictionary.
757760
*/
758761
static bool
759762
instance_has_key(PyObject *obj, PyObject *name, uint32_t *shared_keys_version)
@@ -762,7 +765,8 @@ instance_has_key(PyObject *obj, PyObject *name, uint32_t *shared_keys_version)
762765
if ((cls->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
763766
return false;
764767
}
765-
if (cls->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
768+
if ((cls->tp_flags & Py_TPFLAGS_INLINE_VALUES) &&
769+
FT_ATOMIC_LOAD_UINT8(_PyObject_InlineValues(obj)->valid)) {
766770
PyDictKeysObject *keys = ((PyHeapTypeObject *)cls)->ht_cached_keys;
767771
Py_ssize_t index =
768772
_PyDictKeys_StringLookupAndVersion(keys, name, shared_keys_version);
@@ -1281,14 +1285,14 @@ specialize_attr_loadclassattr(PyObject *owner, _Py_CODEUNIT *instr,
12811285

12821286
unsigned long tp_flags = PyType_GetFlags(owner_cls);
12831287
if (tp_flags & Py_TPFLAGS_INLINE_VALUES) {
1284-
#ifndef Py_GIL_DISABLED
1285-
assert(_PyDictKeys_StringLookup(
1286-
((PyHeapTypeObject *)owner_cls)->ht_cached_keys, name) < 0);
1287-
#endif
12881288
if (shared_keys_version == 0) {
12891289
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OUT_OF_VERSIONS);
12901290
return 0;
12911291
}
1292+
#ifndef Py_GIL_DISABLED
1293+
assert(_PyDictKeys_StringLookup(
1294+
((PyHeapTypeObject *)owner_cls)->ht_cached_keys, name) < 0);
1295+
#endif
12921296
specialize(instr, is_method ? LOAD_ATTR_METHOD_WITH_VALUES : LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES);
12931297
}
12941298
else {

0 commit comments

Comments
 (0)