Skip to content

Commit d1bb94d

Browse files
markshannonGlyphack
authored andcommitted
pythonGH-109369: Add machinery for deoptimizing tier2 executors, both individually and globally. (pythonGH-110384)
1 parent 64491a0 commit d1bb94d

File tree

7 files changed

+353
-2
lines changed

7 files changed

+353
-2
lines changed

Include/cpython/optimizer.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,27 @@
66
extern "C" {
77
#endif
88

9+
typedef struct _PyExecutorLinkListNode {
10+
struct _PyExecutorObject *next;
11+
struct _PyExecutorObject *previous;
12+
} _PyExecutorLinkListNode;
13+
14+
15+
/* Bloom filter with m = 256
16+
* https://en.wikipedia.org/wiki/Bloom_filter */
17+
#define BLOOM_FILTER_WORDS 8
18+
19+
typedef struct _bloom_filter {
20+
uint32_t bits[BLOOM_FILTER_WORDS];
21+
} _PyBloomFilter;
22+
923
typedef struct {
1024
uint8_t opcode;
1125
uint8_t oparg;
26+
uint8_t valid;
27+
uint8_t linked;
28+
_PyBloomFilter bloom;
29+
_PyExecutorLinkListNode links;
1230
} _PyVMData;
1331

1432
typedef struct _PyExecutorObject {
@@ -45,6 +63,14 @@ _PyOptimizer_BackEdge(struct _PyInterpreterFrame *frame, _Py_CODEUNIT *src, _Py_
4563

4664
extern _PyOptimizerObject _PyOptimizer_Default;
4765

66+
void _Py_ExecutorInit(_PyExecutorObject *, _PyBloomFilter *);
67+
void _Py_ExecutorClear(_PyExecutorObject *);
68+
void _Py_BloomFilter_Init(_PyBloomFilter *);
69+
void _Py_BloomFilter_Add(_PyBloomFilter *bloom, void *obj);
70+
PyAPI_FUNC(void) _Py_Executor_DependsOn(_PyExecutorObject *executor, void *obj);
71+
PyAPI_FUNC(void) _Py_Executors_InvalidateDependency(PyInterpreterState *interp, void *obj);
72+
extern void _Py_Executors_InvalidateAll(PyInterpreterState *interp);
73+
4874
/* For testing */
4975
PyAPI_FUNC(PyObject *)PyUnstable_Optimizer_NewCounter(void);
5076
PyAPI_FUNC(PyObject *)PyUnstable_Optimizer_NewUOpOptimizer(void);

Include/internal/pycore_interp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ struct _is {
215215
struct types_state types;
216216
struct callable_cache callable_cache;
217217
_PyOptimizerObject *optimizer;
218+
_PyExecutorObject *executor_list_head;
218219
uint16_t optimizer_resume_threshold;
219220
uint16_t optimizer_backedge_threshold;
220221
uint32_t next_func_version;

Lib/test/test_capi/test_misc.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,6 +2489,67 @@ def get_first_executor(func):
24892489
return None
24902490

24912491

2492+
class TestExecutorInvalidation(unittest.TestCase):
2493+
2494+
def setUp(self):
2495+
self.old = _testinternalcapi.get_optimizer()
2496+
self.opt = _testinternalcapi.get_counter_optimizer()
2497+
_testinternalcapi.set_optimizer(self.opt)
2498+
2499+
def tearDown(self):
2500+
_testinternalcapi.set_optimizer(self.old)
2501+
2502+
def test_invalidate_object(self):
2503+
# Generate a new set of functions at each call
2504+
ns = {}
2505+
func_src = "\n".join(
2506+
f"""
2507+
def f{n}():
2508+
for _ in range(1000):
2509+
pass
2510+
""" for n in range(5)
2511+
)
2512+
exec(textwrap.dedent(func_src), ns, ns)
2513+
funcs = [ ns[f'f{n}'] for n in range(5)]
2514+
objects = [object() for _ in range(5)]
2515+
2516+
for f in funcs:
2517+
f()
2518+
executors = [get_first_executor(f) for f in funcs]
2519+
# Set things up so each executor depends on the objects
2520+
# with an equal or lower index.
2521+
for i, exe in enumerate(executors):
2522+
self.assertTrue(exe.valid)
2523+
for obj in objects[:i+1]:
2524+
_testinternalcapi.add_executor_dependency(exe, obj)
2525+
self.assertTrue(exe.valid)
2526+
# Assert that the correct executors are invalidated
2527+
# and check that nothing crashes when we invalidate
2528+
# an executor mutliple times.
2529+
for i in (4,3,2,1,0):
2530+
_testinternalcapi.invalidate_executors(objects[i])
2531+
for exe in executors[i:]:
2532+
self.assertFalse(exe.valid)
2533+
for exe in executors[:i]:
2534+
self.assertTrue(exe.valid)
2535+
2536+
def test_uop_optimizer_invalidation(self):
2537+
# Generate a new function at each call
2538+
ns = {}
2539+
exec(textwrap.dedent("""
2540+
def f():
2541+
for i in range(1000):
2542+
pass
2543+
"""), ns, ns)
2544+
f = ns['f']
2545+
opt = _testinternalcapi.get_uop_optimizer()
2546+
with temporary_optimizer(opt):
2547+
f()
2548+
exe = get_first_executor(f)
2549+
self.assertTrue(exe.valid)
2550+
_testinternalcapi.invalidate_executors(f.__code__)
2551+
self.assertFalse(exe.valid)
2552+
24922553
class TestUops(unittest.TestCase):
24932554

24942555
def test_basic_loop(self):

Modules/_testinternalcapi.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,32 @@ get_executor(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
10021002
return (PyObject *)PyUnstable_GetExecutor((PyCodeObject *)code, ioffset);
10031003
}
10041004

1005+
static PyObject *
1006+
add_executor_dependency(PyObject *self, PyObject *args)
1007+
{
1008+
PyObject *exec;
1009+
PyObject *obj;
1010+
if (!PyArg_ParseTuple(args, "OO", &exec, &obj)) {
1011+
return NULL;
1012+
}
1013+
/* No way to tell in general if exec is an executor, so we only accept
1014+
* counting_executor */
1015+
if (strcmp(Py_TYPE(exec)->tp_name, "counting_executor")) {
1016+
PyErr_SetString(PyExc_TypeError, "argument must be a counting_executor");
1017+
return NULL;
1018+
}
1019+
_Py_Executor_DependsOn((_PyExecutorObject *)exec, obj);
1020+
Py_RETURN_NONE;
1021+
}
1022+
1023+
static PyObject *
1024+
invalidate_executors(PyObject *self, PyObject *obj)
1025+
{
1026+
PyInterpreterState *interp = PyInterpreterState_Get();
1027+
_Py_Executors_InvalidateDependency(interp, obj);
1028+
Py_RETURN_NONE;
1029+
}
1030+
10051031
static int _pending_callback(void *arg)
10061032
{
10071033
/* we assume the argument is callable object to which we own a reference */
@@ -1565,6 +1591,8 @@ static PyMethodDef module_functions[] = {
15651591
{"get_executor", _PyCFunction_CAST(get_executor), METH_FASTCALL, NULL},
15661592
{"get_counter_optimizer", get_counter_optimizer, METH_NOARGS, NULL},
15671593
{"get_uop_optimizer", get_uop_optimizer, METH_NOARGS, NULL},
1594+
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
1595+
{"invalidate_executors", invalidate_executors, METH_O, NULL},
15681596
{"pending_threadfunc", _PyCFunction_CAST(pending_threadfunc),
15691597
METH_VARARGS | METH_KEYWORDS},
15701598
{"pending_identify", pending_identify, METH_VARARGS, NULL},

Python/instrumentation.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,7 @@ _Py_Instrument(PyCodeObject *code, PyInterpreterState *interp)
15821582
if (code->co_executors != NULL) {
15831583
_PyCode_Clear_Executors(code);
15841584
}
1585+
_Py_Executors_InvalidateDependency(interp, code);
15851586
int code_len = (int)Py_SIZE(code);
15861587
/* code->_co_firsttraceable >= code_len indicates
15871588
* that no instrumentation can be inserted.
@@ -1803,6 +1804,7 @@ _PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events)
18031804
return -1;
18041805
}
18051806
set_global_version(interp, new_version);
1807+
_Py_Executors_InvalidateAll(interp);
18061808
return instrument_all_executing_code_objects(interp);
18071809
}
18081810

@@ -1832,6 +1834,7 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
18321834
/* Force instrumentation update */
18331835
code->_co_instrumentation_version -= MONITORING_VERSION_INCREMENT;
18341836
}
1837+
_Py_Executors_InvalidateDependency(interp, code);
18351838
if (_Py_Instrument(code, interp)) {
18361839
return -1;
18371840
}

0 commit comments

Comments
 (0)