Skip to content

Commit 035dd15

Browse files
committed
gh-158649: Extend BINARY_OP_SUBSCR_USTR_INT to latin-1 characters
The specialized instruction returned an interned one-character string only for ASCII characters and exited to the generic path for anything else, so every accented character in otherwise latin-1 text missed its guard. The runtime also interns the 128 latin-1 characters, so widen the guard to 256 and pick the result from either table. Add assert_specialization_stable to test_opcache, which compares the adaptive counters of a function's specialized instructions before and after extra calls, and use it to check that reading a latin-1 string hits on every character.
1 parent 1a85213 commit 035dd15

6 files changed

Lines changed: 71 additions & 9 deletions

File tree

‎Lib/test/test_opcache.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,43 @@ 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 guard miss advances the instruction's adaptive counter. f should
59+
have a fresh code object (see reset_code()), otherwise leftover
60+
specializations from earlier runs can show up as misses.
61+
"""
62+
before = self.adaptive_counters(f)
63+
self.assertTrue(before, f"{f.__qualname__} has no specialized "
64+
"instructions")
65+
for _ in range(calls):
66+
f(*args)
67+
after = self.adaptive_counters(f)
68+
# Ignore instructions that only specialize during these calls.
69+
moved = []
70+
for off, (op, counter) in before.items():
71+
if after.get(off) != (op, counter):
72+
now = after[off][1] if off in after else "unspecialized"
73+
moved.append(f"{op} at offset {off}: counter {counter} -> {now}")
74+
self.assertEqual(moved, [],
75+
f"specialized instructions in {f.__qualname__} "
76+
f"missed their guard during {calls} calls")
77+
4178

4279
class TestLoadSuperAttrCache(unittest.TestCase):
4380
def test_descriptor_not_double_executed_on_spec_fail(self):
@@ -1977,6 +2014,19 @@ def binary_subscr_str_int_non_compact():
19772014
self.assert_specialized(binary_subscr_str_int_non_compact, "BINARY_OP_SUBSCR_USTR_INT")
19782015
self.assert_no_opcode(binary_subscr_str_int_non_compact, "BINARY_OP_SUBSCR_STR_INT")
19792016

2017+
@reset_code
2018+
def binary_subscr_str_int_latin1():
2019+
# Latin-1 characters are interned too, so reads must not miss.
2020+
for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
2021+
a = "olá mundo, ça va?"
2022+
for idx, expected in enumerate(a):
2023+
self.assertEqual(a[idx], expected)
2024+
2025+
binary_subscr_str_int_latin1()
2026+
self.assert_specialized(binary_subscr_str_int_latin1, "BINARY_OP_SUBSCR_USTR_INT")
2027+
self.assert_no_opcode(binary_subscr_str_int_latin1, "BINARY_OP_SUBSCR_STR_INT")
2028+
self.assert_specialization_stable(binary_subscr_str_int_latin1)
2029+
19802030
def binary_subscr_getitems():
19812031
class C:
19822032
def __init__(self, val):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Speed up subscripting a non-ASCII string with an integer when the character
2+
read is latin-1. ``BINARY_OP_SUBSCR_USTR_INT`` now returns the interned
3+
one-character string for characters below 256 instead of only ASCII, so
4+
reads of accented characters no longer fall back to the generic path.

‎Modules/_testinternalcapi/test_cases.c.h‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Python/bytecodes.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,11 +1224,13 @@ dummy_func(
12241224
EXIT_IF(!_PyLong_IsNonNegativeCompact((PyLongObject*)sub));
12251225
Py_ssize_t index = ((PyLongObject*)sub)->long_value.ob_digit[0];
12261226
EXIT_IF(PyUnicode_GET_LENGTH(str) <= index);
1227-
// Specialize for reading an ASCII character from any string:
1227+
// Interned one-character strings exist for latin-1 only:
12281228
Py_UCS4 c = PyUnicode_READ_CHAR(str, index);
1229-
EXIT_IF(Py_ARRAY_LENGTH(_Py_SINGLETON(strings).ascii) <= c);
1229+
EXIT_IF(c >= 256);
12301230
STAT_INC(BINARY_OP, hit);
1231-
PyObject *res_o = (PyObject*)&_Py_SINGLETON(strings).ascii[c];
1231+
PyObject *res_o = (c < 128)
1232+
? (PyObject*)&_Py_SINGLETON(strings).ascii[c]
1233+
: (PyObject*)&_Py_SINGLETON(strings).latin1[c - 128];
12321234
s = str_st;
12331235
i = sub_st;
12341236
INPUTS_DEAD();

‎Python/executor_cases.c.h‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Python/generated_cases.c.h‎

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)