Skip to content

Commit 55402d3

Browse files
saulshanabrookdpdaniblurb-it[bot]brandtbucherFidget-Spinner
authored
gh-119258: Eliminate Type Guards in Tier 2 Optimizer with Watcher (GH-119365)
Co-authored-by: parmeggiani <parmeggiani@spaziodati.eu> Co-authored-by: dpdani <git@danieleparmeggiani.me> Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Brandt Bucher <brandtbucher@microsoft.com> Co-authored-by: Ken Jin <kenjin@python.org>
1 parent 2080425 commit 55402d3

13 files changed

+366
-59
lines changed

Include/internal/pycore_optimizer.h

+6-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct _Py_UopsSymbol {
3333
int flags; // 0 bits: Top; 2 or more bits: Bottom
3434
PyTypeObject *typ; // Borrowed reference
3535
PyObject *const_val; // Owned reference (!)
36+
unsigned int type_version; // currently stores type version
3637
};
3738

3839
#define UOP_FORMAT_TARGET 0
@@ -123,9 +124,11 @@ extern _Py_UopsSymbol *_Py_uop_sym_new_const(_Py_UOpsContext *ctx, PyObject *con
123124
extern _Py_UopsSymbol *_Py_uop_sym_new_null(_Py_UOpsContext *ctx);
124125
extern bool _Py_uop_sym_has_type(_Py_UopsSymbol *sym);
125126
extern bool _Py_uop_sym_matches_type(_Py_UopsSymbol *sym, PyTypeObject *typ);
127+
extern bool _Py_uop_sym_matches_type_version(_Py_UopsSymbol *sym, unsigned int version);
126128
extern void _Py_uop_sym_set_null(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym);
127129
extern void _Py_uop_sym_set_non_null(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym);
128130
extern void _Py_uop_sym_set_type(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, PyTypeObject *typ);
131+
extern bool _Py_uop_sym_set_type_version(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, unsigned int version);
129132
extern void _Py_uop_sym_set_const(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, PyObject *const_val);
130133
extern bool _Py_uop_sym_is_bottom(_Py_UopsSymbol *sym);
131134
extern int _Py_uop_sym_truthiness(_Py_UopsSymbol *sym);
@@ -138,9 +141,9 @@ extern void _Py_uop_abstractcontext_fini(_Py_UOpsContext *ctx);
138141
extern _Py_UOpsAbstractFrame *_Py_uop_frame_new(
139142
_Py_UOpsContext *ctx,
140143
PyCodeObject *co,
141-
_Py_UopsSymbol **localsplus_start,
142-
int n_locals_already_filled,
143-
int curr_stackentries);
144+
int curr_stackentries,
145+
_Py_UopsSymbol **args,
146+
int arg_len);
144147
extern int _Py_uop_frame_pop(_Py_UOpsContext *ctx);
145148

146149
PyAPI_FUNC(PyObject *) _Py_uop_symbols_test(PyObject *self, PyObject *ignored);

Include/internal/pycore_typeobject.h

+11
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ typedef struct {
6363
PyObject *tp_weaklist;
6464
} managed_static_type_state;
6565

66+
#define TYPE_VERSION_CACHE_SIZE (1<<12) /* Must be a power of 2 */
67+
6668
struct types_state {
6769
/* Used to set PyTypeObject.tp_version_tag.
6870
It starts at _Py_MAX_GLOBAL_TYPE_VERSION_TAG + 1,
@@ -118,6 +120,12 @@ struct types_state {
118120
managed_static_type_state initialized[_Py_MAX_MANAGED_STATIC_EXT_TYPES];
119121
} for_extensions;
120122
PyMutex mutex;
123+
124+
// Borrowed references to type objects whose
125+
// tp_version_tag % TYPE_VERSION_CACHE_SIZE
126+
// once was equal to the index in the table.
127+
// They are cleared when the type object is deallocated.
128+
PyTypeObject *type_version_cache[TYPE_VERSION_CACHE_SIZE];
121129
};
122130

123131

@@ -230,6 +238,9 @@ extern void _PyType_SetFlags(PyTypeObject *self, unsigned long mask,
230238
extern void _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask,
231239
unsigned long flags);
232240

241+
extern unsigned int _PyType_GetVersionForCurrentState(PyTypeObject *tp);
242+
PyAPI_FUNC(void) _PyType_SetVersion(PyTypeObject *tp, unsigned int version);
243+
PyTypeObject *_PyType_LookupByVersion(unsigned int version);
233244

234245
#ifdef __cplusplus
235246
}

Lib/test/test_capi/test_opt.py

+147
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,153 @@ def test_modified_local_is_seen_by_optimized_code(self):
13331333
self.assertIs(type(s), float)
13341334
self.assertEqual(s, 1024.0)
13351335

1336+
def test_guard_type_version_removed(self):
1337+
def thing(a):
1338+
x = 0
1339+
for _ in range(100):
1340+
x += a.attr
1341+
x += a.attr
1342+
return x
1343+
1344+
class Foo:
1345+
attr = 1
1346+
1347+
res, ex = self._run_with_optimizer(thing, Foo())
1348+
opnames = list(iter_opnames(ex))
1349+
self.assertIsNotNone(ex)
1350+
self.assertEqual(res, 200)
1351+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1352+
self.assertEqual(guard_type_version_count, 1)
1353+
1354+
def test_guard_type_version_removed_inlined(self):
1355+
"""
1356+
Verify that the guard type version if we have an inlined function
1357+
"""
1358+
1359+
def fn():
1360+
pass
1361+
1362+
def thing(a):
1363+
x = 0
1364+
for _ in range(100):
1365+
x += a.attr
1366+
fn()
1367+
x += a.attr
1368+
return x
1369+
1370+
class Foo:
1371+
attr = 1
1372+
1373+
res, ex = self._run_with_optimizer(thing, Foo())
1374+
opnames = list(iter_opnames(ex))
1375+
self.assertIsNotNone(ex)
1376+
self.assertEqual(res, 200)
1377+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1378+
self.assertEqual(guard_type_version_count, 1)
1379+
1380+
def test_guard_type_version_not_removed(self):
1381+
"""
1382+
Verify that the guard type version is not removed if we modify the class
1383+
"""
1384+
1385+
def thing(a):
1386+
x = 0
1387+
for i in range(100):
1388+
x += a.attr
1389+
# for the first 90 iterations we set the attribute on this dummy function which shouldn't
1390+
# trigger the type watcher
1391+
# then after 90 it should trigger it and stop optimizing
1392+
# Note that the code needs to be in this weird form so it's optimized inline without any control flow
1393+
setattr((Foo, Bar)[i < 90], "attr", 2)
1394+
x += a.attr
1395+
return x
1396+
1397+
class Foo:
1398+
attr = 1
1399+
1400+
class Bar:
1401+
pass
1402+
1403+
res, ex = self._run_with_optimizer(thing, Foo())
1404+
opnames = list(iter_opnames(ex))
1405+
1406+
self.assertIsNotNone(ex)
1407+
self.assertEqual(res, 219)
1408+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1409+
self.assertEqual(guard_type_version_count, 2)
1410+
1411+
1412+
@unittest.expectedFailure
1413+
def test_guard_type_version_not_removed_escaping(self):
1414+
"""
1415+
Verify that the guard type version is not removed if have an escaping function
1416+
"""
1417+
1418+
def thing(a):
1419+
x = 0
1420+
for i in range(100):
1421+
x += a.attr
1422+
# eval should be escaping and so should cause optimization to stop and preserve both type versions
1423+
eval("None")
1424+
x += a.attr
1425+
return x
1426+
1427+
class Foo:
1428+
attr = 1
1429+
res, ex = self._run_with_optimizer(thing, Foo())
1430+
opnames = list(iter_opnames(ex))
1431+
self.assertIsNotNone(ex)
1432+
self.assertEqual(res, 200)
1433+
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
1434+
# Note: This will actually be 1 for noe
1435+
# https://github.com/python/cpython/pull/119365#discussion_r1626220129
1436+
self.assertEqual(guard_type_version_count, 2)
1437+
1438+
1439+
def test_guard_type_version_executor_invalidated(self):
1440+
"""
1441+
Verify that the executor is invalided on a type change.
1442+
"""
1443+
1444+
def thing(a):
1445+
x = 0
1446+
for i in range(100):
1447+
x += a.attr
1448+
x += a.attr
1449+
return x
1450+
1451+
class Foo:
1452+
attr = 1
1453+
1454+
res, ex = self._run_with_optimizer(thing, Foo())
1455+
self.assertEqual(res, 200)
1456+
self.assertIsNotNone(ex)
1457+
self.assertEqual(list(iter_opnames(ex)).count("_GUARD_TYPE_VERSION"), 1)
1458+
self.assertTrue(ex.is_valid())
1459+
Foo.attr = 0
1460+
self.assertFalse(ex.is_valid())
1461+
1462+
def test_type_version_doesnt_segfault(self):
1463+
"""
1464+
Tests that setting a type version doesn't cause a segfault when later looking at the stack.
1465+
"""
1466+
1467+
# Minimized from mdp.py benchmark
1468+
1469+
class A:
1470+
def __init__(self):
1471+
self.attr = {}
1472+
1473+
def method(self, arg):
1474+
self.attr[arg] = None
1475+
1476+
def fn(a):
1477+
for _ in range(100):
1478+
(_ for _ in [])
1479+
(_ for _ in [a.method(None)])
1480+
1481+
fn(A())
1482+
13361483

13371484
if __name__ == "__main__":
13381485
unittest.main()

Lib/test/test_capi/test_watchers.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,10 @@ class C: pass
282282
self.watch(wid, C)
283283
with catch_unraisable_exception() as cm:
284284
C.foo = "bar"
285-
self.assertEqual(cm.unraisable.err_msg,
286-
f"Exception ignored in type watcher callback #0 for {C!r}")
285+
self.assertEqual(
286+
cm.unraisable.err_msg,
287+
f"Exception ignored in type watcher callback #1 for {C!r}",
288+
)
287289
self.assertIs(cm.unraisable.object, None)
288290
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
289291
self.assert_events([])

Lib/test/test_type_cache.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
# Skip this test if the _testcapi module isn't available.
1212
_testcapi = import_helper.import_module("_testcapi")
13+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
1314
type_get_version = _testcapi.type_get_version
14-
type_assign_specific_version_unsafe = _testcapi.type_assign_specific_version_unsafe
15+
type_assign_specific_version_unsafe = _testinternalcapi.type_assign_specific_version_unsafe
1516
type_assign_version = _testcapi.type_assign_version
1617
type_modified = _testcapi.type_modified
1718

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Eliminate type version guards in the tier two interpreter.
2+
3+
Note that setting the ``tp_version_tag`` manually (which has never been supported) may result in crashes.

Modules/_testcapimodule.c

-17
Original file line numberDiff line numberDiff line change
@@ -2403,21 +2403,6 @@ type_modified(PyObject *self, PyObject *type)
24032403
Py_RETURN_NONE;
24042404
}
24052405

2406-
// Circumvents standard version assignment machinery - use with caution and only on
2407-
// short-lived heap types
2408-
static PyObject *
2409-
type_assign_specific_version_unsafe(PyObject *self, PyObject *args)
2410-
{
2411-
PyTypeObject *type;
2412-
unsigned int version;
2413-
if (!PyArg_ParseTuple(args, "Oi:type_assign_specific_version_unsafe", &type, &version)) {
2414-
return NULL;
2415-
}
2416-
assert(!PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE));
2417-
type->tp_version_tag = version;
2418-
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
2419-
Py_RETURN_NONE;
2420-
}
24212406

24222407
static PyObject *
24232408
type_assign_version(PyObject *self, PyObject *type)
@@ -3427,8 +3412,6 @@ static PyMethodDef TestMethods[] = {
34273412
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
34283413
{"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")},
34293414
{"type_modified", type_modified, METH_O, PyDoc_STR("PyType_Modified")},
3430-
{"type_assign_specific_version_unsafe", type_assign_specific_version_unsafe, METH_VARARGS,
3431-
PyDoc_STR("forcefully assign type->tp_version_tag")},
34323415
{"type_assign_version", type_assign_version, METH_O, PyDoc_STR("PyUnstable_Type_AssignVersionTag")},
34333416
{"type_get_tp_bases", type_get_tp_bases, METH_O},
34343417
{"type_get_tp_mro", type_get_tp_mro, METH_O},

Modules/_testinternalcapi.c

+19
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,22 @@ has_inline_values(PyObject *self, PyObject *obj)
20022002
}
20032003

20042004

2005+
// Circumvents standard version assignment machinery - use with caution and only on
2006+
// short-lived heap types
2007+
static PyObject *
2008+
type_assign_specific_version_unsafe(PyObject *self, PyObject *args)
2009+
{
2010+
PyTypeObject *type;
2011+
unsigned int version;
2012+
if (!PyArg_ParseTuple(args, "Oi:type_assign_specific_version_unsafe", &type, &version)) {
2013+
return NULL;
2014+
}
2015+
assert(!PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE));
2016+
_PyType_SetVersion(type, version);
2017+
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
2018+
Py_RETURN_NONE;
2019+
}
2020+
20052021
/*[clinic input]
20062022
gh_119213_getargs
20072023
@@ -2102,6 +2118,9 @@ static PyMethodDef module_functions[] = {
21022118
{"get_rare_event_counters", get_rare_event_counters, METH_NOARGS},
21032119
{"reset_rare_event_counters", reset_rare_event_counters, METH_NOARGS},
21042120
{"has_inline_values", has_inline_values, METH_O},
2121+
{"type_assign_specific_version_unsafe", type_assign_specific_version_unsafe, METH_VARARGS,
2122+
PyDoc_STR("forcefully assign type->tp_version_tag")},
2123+
21052124
#ifdef Py_GIL_DISABLED
21062125
{"py_thread_id", get_py_thread_id, METH_NOARGS},
21072126
#endif

0 commit comments

Comments
 (0)