8000 gh-119258: Eliminate Type Guards in Tier 2 Optimizer with Watcher by saulshanabrook · Pull Request #119365 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content
10000

gh-119258: Eliminate Type Guards in Tier 2 Optimizer with Watcher #119365

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e7b7d5d
Add type version and offset to optimizer header
saulshanabrook May 20, 2024
c902276
Start adding type version
saulshanabrook May 20, 2024
89e3649
Add type version functions and struct initialization
saulshanabrook May 20, 2024
b8525be
fix typo
saulshanabrook May 20, 2024
4bd1608
Add failing test case
saulshanabrook May 20, 2024
ddbfd94
breakpoints and prints
dpdani May 20, 2024
f22130b
Add function definitions
saulshanabrook May 20, 2024
77bc293
wooohoooo
dpdani May 20, 2024
a2ebc49
unprint
dpdani May 20, 2024
e982837
Remove generated printf
saulshanabrook May 20, 2024
0e7e7eb
Remove test breakpoints
saulshanabrook May 20, 2024
4060ab8
📜🤖 Added by blurb_it.
blurb-it[bot] May 20, 2024
1520928
Revert "📜🤖 Added by blurb_it."
saulshanabrook May 20, 2024
5e0792b
Use global type watcher to invalidate type versions
saulshanabrook May 21, 2024
fd979f0
Revert formatting changes to tests
saulshanabrook May 21, 2024
5da1a40
Add additional test
saulshanabrook May 21, 2024
84860ea
Remove printf and make slightly more threadsafe
saulshanabrook May 21, 2024
c12bb90
fix typo
saulshanabrook May 21, 2024
53ab262
Update type watcher ID in test bc we know have a default one at 0
saulshanabrook May 22, 2024
88e2e59
Fix panic and add test for it
saulshanabrook May 22, 2024
1eabca9
add manual cast
saulshanabrook May 22, 2024
1f7cc74
Properly fixed the frame creation with discussion with Ken Jin
saulshanabrook May 22, 2024
f892ea7
Change type_assign_specific_version_unsafe to use safe setting
saulshanabrook May 23, 2024
06ed18f
📜🤖 Added by blurb_it.
blurb-it[bot] May 23, 2024
1343571
Fix more places where tp_version_tag is being set manually
brandtbucher May 23, 2024
2ee5126
Merge branch 'main' into optimizer-type-version-watcher
brandtbucher May 23, 2024
37aeea5
_PyType_ClearCodeByVersion isn't necessary
brandtbucher May 23, 2024
9d6171d
Merge branch 'main' into optimizer-type-version-watcher
Fidget-Spinner May 28, 2024
545c40b
Merge branch 'main' into optimizer-type-version-watcher
saulshanabrook May 29, 2024
c4436eb
Rename typ_version to type_version
saulshanabrook Jun 4, 2024
0d0c647
Change type of type_version to unsigned int
saulshanabrook Jun 4, 2024
9b80acb
C formatting fix
saulshanabrook Jun 4, 2024
6ddb1e7
Rephrase test names
saulshanabrook Jun 4, 2024
9d3f914
Rephrase comments
saulshanabrook Jun 4, 2024
3e0a1e7
Make existing inline test clear
saulshanabrook Jun 4, 2024
3420a42
Add a test for escaping behavior, but allow it to fail for now
saulshanabrook Jun 4, 2024
6886b36
Verify that expected type version is non zero
saulshanabrook Jun 4, 2024
376bb5e
Fix typo
saulshanabrook Jun 4, 2024
e025cc7
Assert pytype watch never errors
saulshanabrook Jun 4, 2024
88f2d89
Fix assertion
saulshanabrook Jun 4, 2024
82bc177
Remove unnecessary assert
saulshanabrook Jun 4, 2024
bdf0a4a
Handle if type versions are different
saulshanabrook Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Include/internal/pycore_optimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct _Py_UopsSymbol {
int flags; // 0 bits: Top; 2 or more bits: Bottom
PyTypeObject *typ; // Borrowed reference
PyObject *const_val; // Owned reference (!)
unsigned int type_version; // currently stores type version
};

#define UOP_FORMAT_TARGET 0
Expand Down Expand Up @@ -123,9 +124,11 @@ extern _Py_UopsSymbol *_Py_uop_sym_new_const(_Py_UOpsContext *ctx, PyObject *con
extern _Py_UopsSymbol *_Py_uop_sym_new_null(_Py_UOpsContext *ctx);
extern bool _Py_uop_sym_has_type(_Py_UopsSymbol *sym);
extern bool _Py_uop_sym_matches_type(_Py_UopsSymbol *sym, PyTypeObject *typ);
extern bool _Py_uop_sym_matches_type_version(_Py_UopsSymbol *sym, unsigned int version);
extern void _Py_uop_sym_set_null(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym);
extern void _Py_uop_sym_set_non_null(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym);
extern void _Py_uop_sym_set_type(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, PyTypeObject *typ);
extern bool _Py_uop_sym_set_type_version(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, unsigned int version);
extern void _Py_uop_sym_set_const(_Py_UOpsContext *ctx, _Py_UopsSymbol *sym, PyObject *const_val);
extern bool _Py_uop_sym_is_bottom(_Py_UopsSymbol *sym);
extern int _Py_uop_sym_truthiness(_Py_UopsSymbol *sym);
Expand All @@ -138,9 +141,9 @@ extern void _Py_uop_abstractcontext_fini(_Py_UOpsContext *ctx);
extern _Py_UOpsAbstractFrame *_Py_uop_frame_new(
_Py_UOpsContext *ctx,
PyCodeObject *co,
_Py_UopsSymbol **localsplus_start,
int n_locals_already_filled,
int curr_stackentries);
int curr_stackentries,
_Py_UopsSymbol **args,
int arg_len);
extern int _Py_uop_frame_pop(_Py_UOpsContext *ctx);

PyAPI_FUNC(PyObject *) _Py_uop_symbols_test(PyObject *self, PyObject *ignored);
Expand Down
11 changes: 11 additions & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ typedef struct {
PyObject *tp_weaklist;
} static_builtin_state;

#define TYPE_VERSION_CACHE_SIZE (1<<12) /* Must be a power of 2 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but have we experimented with different values here? Might be useful to try out different values once this lands.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I haven't... That would make sense. I also wonder about adding the possibility of adding some diagnostics metadata for cache misses?


struct types_state {
/* Used to set PyTypeObject.tp_version_tag.
It starts at _Py_MAX_GLOBAL_TYPE_VERSION_TAG + 1,
Expand Down Expand Up @@ -108,6 +110,12 @@ struct types_state {
size_t num_builtins_initialized;
static_builtin_state builtins[_Py_MAX_STATIC_BUILTIN_TYPES];
PyMutex mutex;

// Borrowed references to type objects whose
// tp_version_tag % TYPE_VERSION_CACHE_SIZE
// once was equal to the index in the table.
// They are cleared when the type object is deallocated.
PyTypeObject *type_version_cache[TYPE_VERSION_CACHE_SIZE];
};


Expand Down Expand Up @@ -202,6 +210,9 @@ extern void _PyType_SetFlags(PyTypeObject *self, unsigned long mask,
extern void _PyType_SetFlagsRecursive(PyTypeObject *self, unsigned long mask,
unsigned long flags);

extern unsigned int _PyType_GetVersionForCurrentState(PyTypeObject *tp);
PyAPI_FUNC(void) _PyType_SetVersion(PyTypeObject *tp, unsigned int version);
PyTypeObject *_PyType_LookupByVersion(unsigned int version);

#ifdef __cplusplus
}
Expand Down
147 changes: 147 additions & 0 deletions Lib/test/test_capi/test_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,153 @@ def test_modified_local_is_seen_by_optimized_code(self):
self.assertIs(type(s), float)
self.assertEqual(s, 1024.0)

def test_guard_type_version_removed(self):
def thing(a):
x = 0
for _ in range(100):
x += a.attr
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, 200)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_type_version_count, 1)

def test_guard_type_version_removed_inlined(self):
"""
Verify that the guard type version if we have an inlined function
"""

def fn():
pass

def thing(a):
x = 0
for _ in range(100):
x += a.attr
fn()
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, 200)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_type_version_count, 1)

def test_guard_type_version_not_removed(self):
"""
Verify that the guard type version is not removed if we modify the class
"""

def thing(a):
x = 0
for i in range(100):
x += a.attr
# for the first 90 iterations we set the attribute on this dummy function which shouldn't
# trigger the type watcher
# then after 90 it should trigger it and stop optimizing
# Note that the code needs to be in this weird form so it's optimized inline without any control flow
setattr((Foo, Bar)[i < 90], "attr", 2)
x += a.attr
return x

class Foo:
attr = 1

class Bar:
pass

res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))

self.assertIsNotNone(ex)
self.assertEqual(res, 219)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
self.assertEqual(guard_type_version_count, 2)


@unittest.expectedFailure
def test_guard_type_version_not_removed_escaping(self):
"""
Verify that the guard type version is not removed if have an escaping function
"""

def thing(a):
x = 0
for i in range(100):
x += a.attr
# eval should be escaping and so should cause optimization to stop and preserve both type versions
eval("None")
x += a.attr
return x

class Foo:
attr = 1
res, ex = self._run_with_optimizer(thing, Foo())
opnames = list(iter_opnames(ex))
self.assertIsNotNone(ex)
self.assertEqual(res, 200)
guard_type_version_count = opnames.count("_GUARD_TYPE_VERSION")
# Note: This will actually be 1 for noe
# https://github.com/python/cpython/pull/119365#discussion_r1626220129
self.assertEqual(guard_type_version_count, 2)


def test_guard_type_version_executor_invalidated(self):
"""
Verify that the executor is invalided on a type change.
"""

def thing(a):
x = 0
for i in range(100):
x += a.attr
x += a.attr
return x

class Foo:
attr = 1

res, ex = self._run_with_optimizer(thing, Foo())
self.assertEqual(res, 200)
self.assertIsNotNone(ex)
self.assertEqual(list(iter_opnames(ex)).count("_GUARD_TYPE_VERSION"), 1)
self.assertTrue(ex.is_valid())
Foo.attr = 0
self.assertFalse(ex.is_valid())

def test_type_version_doesnt_segfault(self):
"""
Tests that setting a type version doesn't cause a segfault when later looking at the stack.
"""

# Minimized from mdp.py benchmark

class A:
def __init__(self):
self.attr = {}

def method(self, arg):
self.attr[arg] = None

def fn(a):
for _ in range(100):
(_ for _ in [])
(_ for _ in [a.method(None)])

fn(A())


if __name__ == "__main__":
unittest.main()
6 changes: 4 additions & 2 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,10 @@ class C: pass
self.watch(wid, C)
with catch_unraisable_exception() as cm:
C.foo = "bar"
self.assertEqual(cm.unraisable.err_msg,
f"Exception ignored in type watcher callback #0 for {C!r}")
self.assertEqual(
cm.unraisable.err_msg,
f"Exception ignored in type watcher callback #1 for {C!r}",
)
self.assertIs(cm.unraisable.object, None)
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
self.assert_events([])
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_type_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

# Skip this test if the _testcapi module isn't available.
_testcapi = import_helper.import_module("_testcapi")
_testinternalcapi = import_helper.import_module("_testinternalcapi")
type_get_version = _testcapi.type_get_version
type_assign_specific_version_unsafe = _testcapi.type_assign_specific_version_unsafe
type_assign_specific_version_unsafe = _testinternalcapi.type_assign_specific_version_unsafe
type_assign_version = _testcapi.type_assign_version
type_modified = _testcapi.type_modified

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Eliminate type version guards in the tier two interpreter.

Note that setting the ``tp_version_tag`` manually (which has never been supported) may result in crashes.
17 changes: 0 additions & 17 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2395,21 +2395,6 @@ type_modified(PyObject *self, PyObject *type)
Py_RETURN_NONE;
}

// Circumvents standard version assignment machinery - use with caution and only on
// short-lived heap types
static PyObject *
type_assign_specific_version_unsafe(PyObject *self, PyObject *args)
{
PyTypeObject *type;
unsigned int version;
if (!PyArg_ParseTuple(args, "Oi:type_assign_specific_version_unsafe", &type, &version)) {
return NULL;
}
assert(!PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE));
type->tp_version_tag = version;
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
Py_RETURN_NONE;
}

static PyObject *
type_assign_version(PyObject *self, PyObject *type)
Expand Down Expand Up @@ -3418,8 +3403,6 @@ static PyMethodDef TestMethods[] = {
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
{"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")},
{"type_modified", type_modified, METH_O, PyDoc_STR("PyType_Modified")},
{"type_assign_specific_version_unsafe", type_assign_specific_version_unsafe, METH_VARARGS,
PyDoc_STR("forcefully assign type->tp_version_tag")},
{"type_assign_version", type_assign_version, METH_O, PyDoc_STR("PyUnstable_Type_AssignVersionTag")},
{"type_get_tp_bases", type_get_tp_bases, METH_O},
{"type_get_tp_mro", type_get_tp_mro, METH_O},
Expand Down
19 changes: 19 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,22 @@ has_inline_values(PyObject *self, PyObject *obj)
}


// Circumvents standard version assignment machinery - use with caution and only on
// short-lived heap types
static PyObject *
type_assign_specific_version_unsafe(PyObject *self, PyObject *args)
{
PyTypeObject *type;
unsigned int version;
if (!PyArg_ParseTuple(args, "Oi:type_assign_specific_version_unsafe", &type, &version)) {
return NULL;
}
assert(!PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE));
_PyType_SetVersion(type, version);
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
Py_RETURN_NONE;
}

/*[clinic input]
gh_119213_getargs

Expand Down Expand Up @@ -2107,6 +2123,9 @@ static PyMethodDef module_functions[] = {
{"get_rare_event_counters", get_rare_event_counters, METH_NOARGS},
{"reset_rare_event_counters", reset_rare_event_counters, METH_NOARGS},
{"has_inline_values", has_inline_values, METH_O},
{"type_assign_specific_version_unsafe", type_assign_specific_version_unsafe, METH_VARARGS,
PyDoc_STR("forcefully assign type->tp_version_tag")},

#ifdef Py_GIL_DISABLED
{"py_thread_id", get_py_thread_id, METH_NOARGS},
#endif
Expand Down
Loading
Loading
0