8000 Merge remote-tracking branch 'upstream/main' into pep649-compile · python/cpython@8f486ba · GitHub
[go: up one dir, main page]

Skip to content

Commit 8f486ba

Browse files
committed
Merge remote-tracking branch 'upstream/main' into pep649-compile
2 parents 487ea34 + 38a25e9 commit 8f486ba

19 files changed

+505
-94
lines changed

Include/internal/pycore_optimizer.h

Lines changed: 6 additions & 3 deletions
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

Lines changed: 11 additions & 0 deletions
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/pydoc.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,7 +2034,7 @@ def help(self, request, is_cli=False):
20342034
elif request in self.symbols: self.showsymbol(request)
20352035
elif request in ['True', 'False', 'None']:
20362036
# special case these keywords since they are objects too
2037-
doc(eval(request), 'Help on %s:', is_cli=is_cli)
2037+
doc(eval(request), 'Help on %s:', output=self._output, is_cli=is_cli)
20382038
elif request in self.keywords: self.showtopic(request)
20392039
elif request in self.topics: self.showtopic(request)
20402040
elif request: doc(request, 'Help on %s:', output=self._output, is_cli=is_cli)
@@ -2127,7 +2127,11 @@ def showtopic(self, topic, more_xrefs=''):
21272127
text = 'Related help topics: ' + ', '.join(xrefs.split()) + '\n'
21282128
wrapped_text = textwrap.wrap(text, 72)
21292129
doc += '\n%s\n' % '\n'.join(wrapped_text)
2130-
pager(doc, f'Help on {topic!s}')
2130+
2131+
if self._output is None:
2132+
pager(doc, f'Help on {topic!s}')
2133+
else:
2134+
self.output.write(doc)
21312135

21322136
def _gettopic(self, topic, more_xrefs=''):
21332137
"""Return unbuffered tuple of (topic, xrefs).

Lib/test/test_capi/test_opt.py

Lines changed: 147 additions & 0 deletions
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+
1241 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

Lines changed: 4 additions & 2 deletions
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_datetime.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
import sys
3+
import functools
34

45
from test.support.import_helper import import_fresh_module
56

@@ -39,21 +40,26 @@ def load_tests(loader, tests, pattern):
3940
for cls in test_classes:
4041
cls.__name__ += suffix
4142
cls.__qualname__ += suffix
42-
@classmethod
43-
def setUpClass(cls_, module=module):
44-
cls_._save_sys_modules = sys.modules.copy()
45-
sys.modules[TESTS] = module
46-
sys.modules['datetime'] = module.datetime_module
47-
if hasattr(module, '_pydatetime'):
48-
sys.modules['_pydatetime'] = module._pydatetime
49-
sys.modules['_strptime'] = module._strptime
50-
@classmethod
51-
def tearDownClass(cls_):
52-
sys.modules.clear()
53-
sys.modules.update(cls_._save_sys_modules)
54-
cls.setUpClass = setUpClass
55-
cls.tearDownClass = tearDownClass
56-
tests.addTests(loader.loadTestsFromTestCase(cls))
43+
44+
@functools.wraps(cls, updated=())
45+
class Wrapper(cls):
46+
@classmethod
47+
def setUpClass(cls_, module=module):
48+
cls_._save_sys_modules = sys.modules.copy()
49+
sys.modules[TESTS] = module
50+
sys.modules['datetime'] = module.datetime_module
51+
if hasattr(module, '_pydatetime'):
52+
sys.modules['_pydatetime'] = module._pydatetime
53+
sys.modules['_strptime'] = module._strptime
54+
super().setUpClass()
55+
56+
@classmethod
57+
def tearDownClass(cls_):
58+
super().tearDownClass()
59+
sys.modules.clear()
60+
sys.modules.update(cls_._save_sys_modules)
61+
62+
tests.addTests(loader.loadTestsFromTestCase(Wrapper))
5763
return tests
5864

5965

0 commit comments

Comments
 (0)
0