Skip to content

Commit 89dedf5

Browse files
Carl Meyerfacebook-github-bot
authored andcommitted
use func watchers API for funcs modified and destroyed
Summary: Register a function watcher for the JIT, and use its callback to handle func-modified and func-destroyed events. I'm leaving func-initialization to a separate PR; it will require more involved handling, because we'll also want to visit GC objects to find functions created before the JIT was initialized. I eliminated the call to `PyEntry_init` after a change to function defaults. This only happened if the function was not JIT-compiled. I think this dates back to Cinder 3.8 where we had many more (non-JIT) function entrypoint variants, depending on number of arguments, argument defaults, etc, and needed to ensure we set the right one. But in 3.10 we eliminated all those custom entry points, so I don't think this is needed anymore. I had to make a fix to the func watchers tests so they would work correctly when running with another func watcher active. I also submitted this fix upstream: python/cpython#106286 And I had to delete a C++ test that was passing only due to a series of accidents. The func-modified callbacks (both before and after this diff) are global and dispatch only to the global singleton `jit_ctx` in `pyjit.cpp`, so they can't be tested correctly by a unit test that never globally enables the JIT and only constructs its own private JIT context. The function-modified callback in this test was doing nothing, but the entrypoint of the function was getting re-set to `_PyFunction_Vectorcall` anyway due to `PyEntry_init` seeing the JIT as not enabled; this seems unlikely to be a realistic scenario the test was intended to check. There is already a Python-level test (`test_funcattrs.FunctionPropertiesTest.test_copying___code__`) that verifies that re-assigning `__code__` changes the behavior of the function; we run this test under the JIT, and it fails if we fail to deopt the function on `__code__` reassignment. So the behavior we care about is already tested. Reviewed By: alexmalyshev Differential Revision: D47156535 fbshipit-source-id: ba15f93800e23b33eb12262a201d24360df39a67
1 parent 3767da7 commit 89dedf5

File tree

5 files changed

+100
-76
lines changed

5 files changed

+100
-76
lines changed

Jit/pyjit.cpp

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ struct JitConfig {
6565
int hir_inliner_enabled{0};
6666
unsigned int auto_jit_threshold{0};
6767
int dict_watcher_id{-1};
68+
int func_watcher_id{-1};
6869
};
6970
static JitConfig jit_config;
7071

@@ -1712,6 +1713,24 @@ static int install_jit_audit_hook() {
17121713
return -1;
17131714
}
17141715

1716+
static int install_jit_func_watcher() {
1717+
int watcher_id = PyFunction_AddWatcher(_PyJIT_FuncWatcher);
1718+
if (watcher_id < 0) {
1719+
return -1;
1720+
}
1721+
jit_config.func_watcher_id = watcher_id;
1722+
return 0;
1723+
}
1724+
1725+
static void clear_jit_func_watcher() {
1726+
if (jit_config.func_watcher_id >= 0) {
1727+
if (PyFunction_ClearWatcher(jit_config.func_watcher_id) < 0) {
1728+
PyErr_WriteUnraisable(Py_None);
1729+
}
1730+
jit_config.func_watcher_id = -1;
1731+
}
1732+
}
1733+
17151734
static int install_jit_dict_watcher() {
17161735
int watcher_id = PyDict_AddWatcher(_PyJIT_DictWatcher);
17171736
if (watcher_id < 0) {
@@ -1721,6 +1740,15 @@ static int install_jit_dict_watcher() {
17211740
return 0;
17221741
}
17231742

1743+
static void clear_jit_dict_watcher() {
1744+
if (jit_config.dict_watcher_id >= 0) {
1745+
if (PyDict_ClearWatcher(jit_config.dict_watcher_id) < 0) {
1746+
PyErr_WriteUnraisable(Py_None);
1747+
}
1748+
jit_config.dict_watcher_id = -1;
1749+
}
1750+
}
1751+
17241752
void _PyJIT_WatchDict(PyObject* dict) {
17251753
if (PyDict_Watch(jit_config.dict_watcher_id, dict) < 0) {
17261754
PyErr_Print();
@@ -1736,26 +1764,37 @@ void _PyJIT_UnwatchDict(PyObject* dict) {
17361764
}
17371765

17381766
int _PyJIT_InitializeSubInterp() {
1739-
// HACK: for now we assume we are the only dict watcher out there, so that we
1740-
// can just keep track of a single dict watcher ID rather than one per
1741-
// interpreter.
1742-
int prev_watcher_id = jit_config.dict_watcher_id;
1767+
// HACK: for now assume we are the only watcher out there, so that we can just
1768+
// keep track of a single watcher ID rather than one per interpreter.
1769+
int prev_dict_watcher_id = jit_config.dict_watcher_id;
17431770
JIT_CHECK(
1744-
prev_watcher_id >= 0,
1771+
prev_dict_watcher_id >= 0,
17451772
"Initializing sub-interpreter without main interpreter?");
17461773
if (install_jit_dict_watcher() < 0) {
17471774
return -1;
17481775
}
17491776
JIT_CHECK(
1750-
jit_config.dict_watcher_id == prev_watcher_id,
1777+
jit_config.dict_watcher_id == prev_dict_watcher_id,
17511778
"Somebody else watching dicts?");
1779+
1780+
// dict watcher is always enabled; func watcher only if JIT is
1781+
int prev_func_watcher_id = jit_config.func_watcher_id;
1782+
if (prev_func_watcher_id >= 0) {
1783+
if (install_jit_func_watcher() < 0) {
1784+
return -1;
1785+
}
1786+
JIT_CHECK(
1787+
jit_config.func_watcher_id == prev_func_watcher_id,
1788+
"Somebody else watching functions?");
1789+
}
1790+
17521791
return 0;
17531792
}
17541793

17551794
int _PyJIT_Initialize() {
17561795
// If we have data symbols which are public but not used within CPython code,
17571796
// we need to ensure the linker doesn't GC the .data section containing them.
1758-
// We can do this by referencing at least symbol from that sourfe module.
1797+
// We can do this by referencing at least symbol from that source module.
17591798
// In future versions of clang/gcc we may be able to eliminate this with
17601799
// 'keep' and/or 'used' attributes.
17611800
//
@@ -1857,7 +1896,8 @@ int _PyJIT_Initialize() {
18571896
return -1;
18581897
}
18591898

1860-
if (install_jit_audit_hook() < 0 || register_fork_callback(mod) < 0) {
1899+
if (install_jit_audit_hook() < 0 || register_fork_callback(mod) < 0 ||
1900+
install_jit_func_watcher() < 0) {
18611901
return -1;
18621902
}
18631903

@@ -2072,6 +2112,34 @@ void _PyJIT_InstanceTypeAssigned(PyTypeObject* old_ty, PyTypeObject* new_ty) {
20722112
}
20732113
}
20742114

2115+
int _PyJIT_FuncWatcher(
2116+
PyFunction_WatchEvent event,
2117+
PyFunctionObject* func,
2118+
PyObject* new_value) {
2119+
switch (event) {
2120+
case PyFunction_EVENT_CREATE:
2121+
// TODO move PyEntry_init setting out of funcobject.c
2122+
break;
2123+
case PyFunction_EVENT_MODIFY_CODE:
2124+
_PyJIT_FuncModified(func);
2125+
// having deopted the func, we want to immediately consider recompiling.
2126+
// func_set_code will assign this again later, but we do it early so
2127+
// PyEntry_init can consider the new code object now
2128+
Py_INCREF(new_value);
2129+
Py_XSETREF(func->func_code, new_value);
2130+
PyEntry_init(func);
2131+
break;
2132+
case PyFunction_EVENT_MODIFY_DEFAULTS:
2133+
break;
2134+
case PyFunction_EVENT_MODIFY_KWDEFAULTS:
2135+
break;
2136+
case PyFunction_EVENT_DESTROY:
2137+
_PyJIT_FuncDestroyed(func);
2138+
break;
2139+
}
2140+
return 0;
2141+
}
2142+
20752143
void _PyJIT_FuncModified(PyFunctionObject* func) {
20762144
if (jit_ctx) {
20772145
_PyJITContext_FuncModified(jit_ctx, func);
@@ -2175,6 +2243,10 @@ int _PyJIT_Finalize() {
21752243
CodeAllocator::freeGlobalCodeAllocator();
21762244
}
21772245

2246+
// now that we've released all references to Python funcs, it's safe to shut
2247+
// down the func watcher
2248+
clear_jit_func_watcher();
2249+
21782250
#define CLEAR_STR(s) Py_CLEAR(s_str_##s);
21792251
INTERNED_STRINGS(CLEAR_STR)
21802252
#undef CLEAR_STR
@@ -2189,13 +2261,7 @@ int _PyJIT_Finalize() {
21892261
Runtime::shutdown();
21902262

21912263
// must happen after Runtime::shutdown() so that we've cleared dict caches
2192-
if (jit_config.dict_watcher_id >= 0) {
2193-
if (PyDict_ClearWatcher(jit_config.dict_watcher_id) < 0) {
2194-
PyErr_Print();
2195-
PyErr_Clear();
2196-
}
2197-
jit_config.dict_watcher_id = -1;
2198-
}
2264+
clear_jit_dict_watcher();
21992265

22002266
return 0;
22012267
}

Jit/pyjit.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ PyAPI_FUNC(int) _PyJIT_DictWatcher(
169169
PyObject* key,
170170
PyObject* new_value);
171171

172+
/*
173+
* Func watcher callback; called on func creation/modification/deallocation.
174+
*/
175+
PyAPI_FUNC(int) _PyJIT_FuncWatcher(
176+
PyFunction_WatchEvent event,
177+
PyFunctionObject* func,
178+
PyObject* new_value);
179+
172180
/*
173181
* Informs the JIT that a type, function, or code object is being created,
174182
* modified, or destroyed.

Modules/_testcapimodule.c

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6254,9 +6254,9 @@ get_dict_watcher_events(PyObject *self, PyObject *Py_UNUSED(args))
62546254

62556255
// Test function watchers
62566256

6257-
#define NUM_FUNC_WATCHERS 2
6258-
static PyObject *pyfunc_watchers[NUM_FUNC_WATCHERS];
6259-
static int func_watcher_ids[NUM_FUNC_WATCHERS] = {-1, -1};
6257+
#define NUM_TEST_FUNC_WATCHERS 2
6258+
static PyObject *pyfunc_watchers[NUM_TEST_FUNC_WATCHERS];
6259+
static int func_watcher_ids[NUM_TEST_FUNC_WATCHERS] = {-1, -1};
62606260

62616261
static PyObject *
62626262
get_id(PyObject *obj)
@@ -6330,7 +6330,7 @@ second_func_watcher_callback(PyFunction_WatchEvent event,
63306330
return call_pyfunc_watcher(pyfunc_watchers[1], event, func, new_value);
63316331
}
63326332

6333-
static PyFunction_WatchCallback func_watcher_callbacks[NUM_FUNC_WATCHERS] = {
6333+
static PyFunction_WatchCallback func_watcher_callbacks[NUM_TEST_FUNC_WATCHERS] = {
63346334
first_func_watcher_callback,
63356335
second_func_watcher_callback
63366336
};
@@ -6355,26 +6355,25 @@ add_func_watcher(PyObject *self, PyObject *func)
63556355
return NULL;
63566356
}
63576357
int idx = -1;
6358-
for (int i = 0; i < NUM_FUNC_WATCHERS; i++) {
6358+
for (int i = 0; i < NUM_TEST_FUNC_WATCHERS; i++) {
63596359
if (func_watcher_ids[i] == -1) {
63606360
idx = i;
63616361
break;
63626362
}
63636363
}
63646364
if (idx == -1) {
6365-
PyErr_SetString(PyExc_RuntimeError, "no free watchers");
6366-
return NULL;
6367-
}
6368-
PyObject *result = PyLong_FromLong(idx);
6369-
if (result == NULL) {
6365+
PyErr_SetString(PyExc_RuntimeError, "no free test watchers");
63706366
return NULL;
63716367
}
63726368
func_watcher_ids[idx] = PyFunction_AddWatcher(func_watcher_callbacks[idx]);
63736369
if (func_watcher_ids[idx] < 0) {
6374-
Py_DECREF(result);
63756370
return NULL;
63766371
}
63776372
pyfunc_watchers[idx] = Py_NewRef(func);
6373+
PyObject *result = PyLong_FromLong(func_watcher_ids[idx]);
6374+
if (result == NULL) {
6375+
return NULL;
6376+
}
63786377
return result;
63796378
}
63806379

@@ -6391,7 +6390,7 @@ clear_func_watcher(PyObject *self, PyObject *watcher_id_obj)
63916390
return NULL;
63926391
}
63936392
int idx = -1;
6394-
for (int i = 0; i < NUM_FUNC_WATCHERS; i++) {
6393+
for (int i = 0; i < NUM_TEST_FUNC_WATCHERS; i++) {
63956394
if (func_watcher_ids[i] == wid) {
63966395
idx = i;
63976396
break;

Objects/funcobject.c

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,6 @@ func_set_code(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
460460
handle_func_event(PyFunction_EVENT_MODIFY_CODE, op, value);
461461
Py_INCREF(value);
462462
Py_XSETREF(op->func_code, value);
463-
#ifdef ENABLE_CINDERX
464-
_PyJIT_FuncModified(op);
465-
PyEntry_init(op);
466-
#endif
467463
return 0;
468464
}
469465

@@ -554,14 +550,6 @@ func_set_defaults(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored
554550
handle_func_event(PyFunction_EVENT_MODIFY_DEFAULTS, op, value);
555551
Py_XINCREF(value);
556552
Py_XSETREF(op->func_defaults, value);
557-
#ifdef ENABLE_CINDERX
558-
// JIT-compiled functions load their defaults at runtime if needed. Others
559-
// need their entrypoint recomputed.
560-
// TODO(T126790232): Don't load defaults at runtime and recompile as needed.
561-
if (!_PyJIT_IsCompiled((PyObject *)op)) {
562-
PyEntry_init(op);
563-
}
564-
#endif
565553
return 0;
566554
}
567555

@@ -783,9 +771,6 @@ func_clear(PyFunctionObject *op)
783771
static void
784772
func_dealloc(PyFunctionObject *op)
785773
{
786-
#ifdef ENABLE_CINDERX
787-
_PyJIT_FuncDestroyed(op);
788-
#endif
789774
assert(Py_REFCNT(op) == 0);
790775
Py_SET_REFCNT(op, 1);
791776
handle_func_event(PyFunction_EVENT_DESTROY, op, NULL);

RuntimeTests/jit_context_test.cpp

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,6 @@ class PyJITContextTest : public RuntimeTest {
2626
_PyJITContext* jit_ctx_;
2727
};
2828

29-
TEST_F(PyJITContextTest, CompiledFunctionsAreDeoptimizedWhenCodeChanges) {
30-
const char* src = R"(
31-
def func():
32-
return 12345
33-
)";
34-
Ref<PyFunctionObject> func(compileAndGet(src, "func"));
35-
ASSERT_NE(func.get(), nullptr) << "Failed creating func";
36-
37-
vectorcallfunc old_entrypoint = func->vectorcall;
38-
_PyJIT_Result st = _PyJITContext_CompileFunction(jit_ctx_, func);
39-
ASSERT_EQ(st, PYJIT_RESULT_OK) << "Failed compiling";
40-
41-
// Create a new function object so that we can grab its code object and
42-
// assign it to the original function, at which point func should be
43-
// de-optimized
44-
const char* src2 = R"(
45-
def func2():
46-
return 2
47-
48-
func.__code__ = func2.__code__
49-
)";
50-
auto globals = Ref<>::steal(PyDict_New());
51-
ASSERT_NE(globals.get(), nullptr) << "Failed creating globals";
52-
ASSERT_EQ(PyDict_SetItemString(globals, "func", func), 0)
53-
<< "Failed updating globals";
54-
55-
auto result =
56-
Ref<>::steal(PyRun_String(src2, Py_file_input, globals, globals));
57-
ASSERT_NE(result.get(), nullptr) << "Failed executing code";
58-
59-
// After de-optimization, the entrypoint should have been restored to the
60-
// original value
61-
ASSERT_EQ(func->vectorcall, old_entrypoint) << "entrypoint wasn't restored";
62-
}
6329

6430
TEST_F(PyJITContextTest, UnwatchableBuiltins) {
6531
// This is a C++ test rather than in test_cinderjit so we can guarantee a

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy