Skip to content

Commit 952d8fb

Browse files
committed
Assert on specialisation stability for subclasses
1 parent 5c7445b commit 952d8fb

1 file changed

Lines changed: 80 additions & 1 deletion

File tree

‎Lib/test/test_opcache.py‎

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,47 @@ def assert_no_opcode(self, f, opname):
3838
opnames = {instruction.opname for instruction in instructions}
3939
self.assertNotIn(opname, opnames)
4040

41+
def adaptive_counters(self, f):
42+
"""Map each specialized instruction in f to its adaptive counter."""
43+
counters = {}
44+
for instruction in dis.get_instructions(f, adaptive=True):
45+
if instruction.opname == instruction.baseopname:
46+
continue
47+
if instruction.baseopname in ("RESUME", "JUMP_BACKWARD"):
48+
continue
49+
cache = {name: data for name, _, data in instruction.cache_info}
50+
if "counter" in cache:
51+
counters[instruction.offset] = (instruction.opname,
52+
cache["counter"])
53+
return counters
54+
55+
def assert_specialization_stable(self, f, *args, calls=10):
56+
"""Assert that no specialized instruction in f misses its guard.
57+
58+
A specialized instruction whose guard holds leaves its adaptive
59+
counter untouched. One whose guard fails on every execution
60+
(the specializer accepted a site its guard then rejects) advances
61+
the counter on each miss, so comparing the counters before and
62+
after a few more calls detects the mismatch without needing a
63+
pystats build.
64+
65+
f must start from a fresh code object (see reset_code()): a nested
66+
function reuses its code object across calls of the enclosing test,
67+
so a specialization left over from a previous run with different
68+
operands would show up here as a miss.
69+
"""
70+
before = self.adaptive_counters(f)
71+
self.assertTrue(before, f"{f.__qualname__} has no specialized "
72+
"instructions")
73+
for _ in range(calls):
74+
f(*args)
75+
after = self.adaptive_counters(f)
76+
moved = [f"{op} at offset {off}: counter {before[off][1]} -> {c}"
77+
for off, (op, c) in after.items() if before[off] != (op, c)]
78+
self.assertEqual(moved, [],
79+
f"specialized instructions in {f.__qualname__} "
80+
f"missed their guard during {calls} calls")
81+
4182

4283
class TestLoadSuperAttrCache(unittest.TestCase):
4384
def test_descriptor_not_double_executed_on_spec_fail(self):
@@ -2156,6 +2197,7 @@ def list_append(l):
21562197
for _ in r:
21572198
l.append(1)
21582199

2200+
reset_code(list_append)
21592201
list_append([])
21602202
self.assert_specialized(list_append, "CALL_LIST_APPEND")
21612203
self.assert_no_opcode(list_append, "CALL_METHOD_DESCRIPTOR_O")
@@ -2166,10 +2208,47 @@ def my_list_append(l):
21662208
l.append(1)
21672209

21682210
class MyList(list): pass
2169-
my_list_append(MyList())
2211+
my_list = MyList()
2212+
reset_code(my_list_append)
2213+
my_list_append(my_list)
21702214
self.assert_specialized(my_list_append, "CALL_METHOD_DESCRIPTOR_O")
21712215
self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND")
21722216
self.assert_no_opcode(my_list_append, "CALL")
2217+
self.assert_specialization_stable(my_list_append, my_list)
2218+
2219+
@cpython_only
2220+
@requires_specialization
2221+
@requires_jit_disabled # JIT-compiled code stops moving tier-1 counters
2222+
def test_call_method_descriptor_subclass_instance(self):
2223+
# A C method inherited from a built-in type is specialized for
2224+
# subclass instances too; each guard must accept the subtype
2225+
# rather than miss on every call. One case per guard, plus the
2226+
# unbound form where the receiver is the first argument.
2227+
class MyStr(str): pass
2228+
Point = collections.namedtuple("Point", "x y")
2229+
counts = collections.defaultdict(int, {1: 2})
2230+
2231+
def noargs(s): return s.upper()
2232+
def o(p): return p.count(1)
2233+
def fast(d): return d.get(1)
2234+
def fast_with_keywords(s): return s.split()
2235+
def unbound(d): return dict.get(d, 1)
2236+
2237+
cases = [
2238+
(noargs, MyStr("abc"), "CALL_METHOD_DESCRIPTOR_NOARGS"),
2239+
(o, Point(1, 2), "CALL_METHOD_DESCRIPTOR_O"),
2240+
(fast, counts, "CALL_METHOD_DESCRIPTOR_FAST"),
2241+
(fast_with_keywords, MyStr("a b"),
2242+
"CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS"),
2243+
(unbound, counts, "CALL_METHOD_DESCRIPTOR_FAST"),
2244+
]
2245+
for f, obj, opname in cases:
2246+
with self.subTest(f=f.__name__, obj=type(obj).__name__):
2247+
reset_code(f)
2248+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
2249+
f(obj)
2250+
self.assert_specialized(f, opname)
2251+
self.assert_specialization_stable(f, obj)
21732252

21742253
@cpython_only
21752254
@requires_specialization

0 commit comments

Comments
 (0)