From a1ef342c2669d2b626ac460091fd0c0fd9368a0b Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 23 Mar 2026 00:53:50 +0800 Subject: [PATCH 1/6] JIT: protect against function version changes --- Lib/test/test_capi/test_opt.py | 26 ++++++++++++++++++++++++++ Objects/funcobject.c | 4 ++++ Python/optimizer_bytecodes.c | 9 +++++++++ Python/optimizer_cases.c.h | 7 +++++++ 4 files changed, 46 insertions(+) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index bd9bc8a1a533c3..a12ee5700a1608 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -4288,9 +4288,35 @@ def g(): PYTHON_JIT="1", PYTHON_JIT_STRESS="1") self.assertEqual(result[0].rc, 0, result) + def test_func_version_watched_and_invalidated(self): + def testfunc(n): + for i in range(n): + # Only works on functions promoted to constants + global_identity_code_will_be_modified(i) + + testfunc(TIER2_THRESHOLD) + + ex = get_first_executor(testfunc) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_PUSH_FRAME", uops) + # Both should be not present, as this is a call + # to a simple function with a known function version. + self.assertNotIn("_CHECK_FUNCTION_VERSION_INLINE", uops) + self.assertNotIn("_CHECK_FUNCTION_VERSION", uops) + + global_identity_code_will_be_modified.__code__ = (lambda a:a).__code__ + ex = get_first_executor(testfunc) + # Invalidated and removed. + self.assertIsNone(ex) + + def global_identity(x): return x +def global_identity_code_will_be_modified(x): + return x + class TestObject: def test(self, *args, **kwargs): return args[0] diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 585c7b9a85412c..1b1acd63ba26d3 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -8,6 +8,7 @@ #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount() +#include "pycore_optimizer.h" #include "pycore_pyerrors.h" // _PyErr_Occurred() #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_stats.h" @@ -63,6 +64,9 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func, case PyFunction_EVENT_MODIFY_DEFAULTS: case PyFunction_EVENT_MODIFY_KWDEFAULTS: case PyFunction_EVENT_MODIFY_QUALNAME: +#if _Py_TIER2 + _Py_Executors_InvalidateDependency(_PyInterpreterState_GET(), func, 1); +#endif RARE_EVENT_INTERP_INC(interp, func_modification); break; default: diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index f2d0e2940d7188..f6300fc0df1d97 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -864,6 +864,15 @@ dummy_func(void) { } op(_CHECK_FUNCTION_VERSION, (func_version/2, callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { + PyObject *func = sym_get_probable_value(callable); + if (func == NULL || !PyFunction_Check(func)) { + ctx->contradiction = true; + ctx->done = true; + break; + } + // This could pass due to a global promoted const. + // So we need to add it to the dependencies on both branches. + _Py_BloomFilter_Add(dependencies, func); if (sym_get_func_version(callable) == func_version) { REPLACE_OP(this_instr, _NOP, 0, 0); } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 860bb02b7a0122..c92eecd0b1af41 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3039,6 +3039,13 @@ JitOptRef callable; callable = stack_pointer[-2 - oparg]; uint32_t func_version = (uint32_t)this_instr->operand0; + PyObject *func = sym_get_probable_value(callable); + if (func == NULL || !PyFunction_Check(func)) { + ctx->contradiction = true; + ctx->done = true; + break; + } + _Py_BloomFilter_Add(dependencies, func); if (sym_get_func_version(callable) == func_version) { REPLACE_OP(this_instr, _NOP, 0, 0); } From dc19d93e7201ee067e0ab04cb6a584cf2b5b7e06 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 23 Mar 2026 01:00:37 +0800 Subject: [PATCH 2/6] pass the interp --- Objects/funcobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 1b1acd63ba26d3..d4858e8b4785e2 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -65,7 +65,7 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func, case PyFunction_EVENT_MODIFY_KWDEFAULTS: case PyFunction_EVENT_MODIFY_QUALNAME: #if _Py_TIER2 - _Py_Executors_InvalidateDependency(_PyInterpreterState_GET(), func, 1); + _Py_Executors_InvalidateDependency(interp, func, 1); #endif RARE_EVENT_INTERP_INC(interp, func_modification); break; From 463ed287cce7c1070e5ea224d9c324dfabe7796f Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 6 Apr 2026 20:48:08 +0800 Subject: [PATCH 3/6] Update Objects/funcobject.c Co-authored-by: Kumar Aditya --- Objects/funcobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index d4858e8b4785e2..628c50462ce397 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -8,7 +8,7 @@ #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_object_deferred.h" // _PyObject_SetDeferredRefcount() -#include "pycore_optimizer.h" +#include "pycore_optimizer.h" // _Py_Executors_InvalidateDependency() #include "pycore_pyerrors.h" // _PyErr_Occurred() #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_stats.h" From 9dd70a02e5792a78508624afe462ea4a03d8ef78 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 13 Apr 2026 01:20:40 +0800 Subject: [PATCH 4/6] remove func version symbol altogether --- to buggy without watchers --- Include/internal/pycore_optimizer.h | 2 - Include/internal/pycore_optimizer_types.h | 1 - Lib/test/test_capi/test_opt.py | 32 +--- Objects/funcobject.c | 3 - Python/optimizer_analysis.c | 2 - Python/optimizer_bytecodes.c | 17 +-- Python/optimizer_cases.c.h | 9 +- Python/optimizer_symbols.c | 169 +--------------------- 8 files changed, 18 insertions(+), 217 deletions(-) diff --git a/Include/internal/pycore_optimizer.h b/Include/internal/pycore_optimizer.h index cf01c620476ff7..e7b688333d9ced 100644 --- a/Include/internal/pycore_optimizer.h +++ b/Include/internal/pycore_optimizer.h @@ -424,8 +424,6 @@ extern PyCodeObject *_Py_uop_sym_get_probable_func_code(JitOptRef sym); extern PyObject *_Py_uop_sym_get_probable_value(JitOptRef sym); extern PyTypeObject *_Py_uop_sym_get_probable_type(JitOptRef sym); extern JitOptRef *_Py_uop_sym_set_stack_depth(JitOptContext *ctx, int stack_depth, JitOptRef *current_sp); -extern uint32_t _Py_uop_sym_get_func_version(JitOptRef ref); -bool _Py_uop_sym_set_func_version(JitOptContext *ctx, JitOptRef ref, uint32_t version); extern void _Py_uop_abstractcontext_init(JitOptContext *ctx, _PyBloomFilter *dependencies); extern void _Py_uop_abstractcontext_fini(JitOptContext *ctx); diff --git a/Include/internal/pycore_optimizer_types.h b/Include/internal/pycore_optimizer_types.h index 8ecfbea387460b..a722652cc8163a 100644 --- a/Include/internal/pycore_optimizer_types.h +++ b/Include/internal/pycore_optimizer_types.h @@ -36,7 +36,6 @@ typedef enum _JitSymType { JIT_SYM_NON_NULL_TAG = 3, JIT_SYM_BOTTOM_TAG = 4, JIT_SYM_TYPE_VERSION_TAG = 5, - JIT_SYM_FUNC_VERSION_TAG = 6, JIT_SYM_KNOWN_CLASS_TAG = 7, JIT_SYM_KNOWN_VALUE_TAG = 8, JIT_SYM_TUPLE_TAG = 9, diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 71ad0733ed306e..5eedbc3d27dc76 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -1622,9 +1622,8 @@ def testfunc(n): self.assertEqual(uops.count("_PUSH_FRAME"), 2) # Type version propagation: one guard covers both method lookups self.assertEqual(uops.count("_GUARD_TYPE_VERSION"), 1) - # Function checks eliminated (type info resolves the callable) - self.assertNotIn("_CHECK_FUNCTION_VERSION", uops) - self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops) + # Function checks cannot be eliminated for safety reasons. + self.assertIn("_CHECK_FUNCTION_VERSION", uops) def test_method_chain_guard_elimination(self): """ @@ -1669,10 +1668,7 @@ def testfunc(n): self.assertIsNotNone(ex) uops = get_opnames(ex) self.assertIn("_PUSH_FRAME", uops) - # Both should be not present, as this is a call - # to a simple function with a known function version. - self.assertNotIn("_CHECK_FUNCTION_VERSION_INLINE", uops) - self.assertNotIn("_CHECK_FUNCTION_VERSION", uops) + self.assertIn("_CHECK_FUNCTION_VERSION", uops) # Removed guard self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops) @@ -5178,28 +5174,6 @@ def g(): PYTHON_JIT="1", PYTHON_JIT_STRESS="1") self.assertEqual(result[0].rc, 0, result) - def test_func_version_watched_and_invalidated(self): - def testfunc(n): - for i in range(n): - # Only works on functions promoted to constants - global_identity_code_will_be_modified(i) - - testfunc(TIER2_THRESHOLD) - - ex = get_first_executor(testfunc) - self.assertIsNotNone(ex) - uops = get_opnames(ex) - self.assertIn("_PUSH_FRAME", uops) - # Both should be not present, as this is a call - # to a simple function with a known function version. - self.assertNotIn("_CHECK_FUNCTION_VERSION_INLINE", uops) - self.assertNotIn("_CHECK_FUNCTION_VERSION", uops) - - global_identity_code_will_be_modified.__code__ = (lambda a:a).__code__ - ex = get_first_executor(testfunc) - # Invalidated and removed. - self.assertIsNone(ex) - def test_call_super(self): class A: def method1(self): diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 3d8fae73af6048..94f102ff9329cb 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -64,9 +64,6 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func, case PyFunction_EVENT_MODIFY_DEFAULTS: case PyFunction_EVENT_MODIFY_KWDEFAULTS: case PyFunction_EVENT_MODIFY_QUALNAME: -#if _Py_TIER2 - _Py_Executors_InvalidateDependency(interp, func, 1); -#endif RARE_EVENT_INTERP_INC(interp, func_modification); break; default: diff --git a/Python/optimizer_analysis.c b/Python/optimizer_analysis.c index 6742488a0d06c2..6b48f89510ef28 100644 --- a/Python/optimizer_analysis.c +++ b/Python/optimizer_analysis.c @@ -283,8 +283,6 @@ add_op(JitOptContext *ctx, _PyUOpInstruction *this_instr, #define sym_get_probable_func_code _Py_uop_sym_get_probable_func_code #define sym_get_probable_value _Py_uop_sym_get_probable_value #define sym_set_stack_depth(DEPTH, SP) _Py_uop_sym_set_stack_depth(ctx, DEPTH, SP) -#define sym_get_func_version _Py_uop_sym_get_func_version -#define sym_set_func_version _Py_uop_sym_set_func_version /* Comparison oparg masks */ #define COMPARE_LT_MASK 2 diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index d563e5135dacf2..297c5285e48b79 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -1002,20 +1002,17 @@ dummy_func(void) { op(_CHECK_FUNCTION_VERSION, (func_version/2, callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { PyObject *func = sym_get_probable_value(callable); - if (func == NULL || !PyFunction_Check(func)) { + if (func == NULL || !PyFunction_Check(func) || ((PyFunctionObject *)func)->func_version != func_version) { ctx->contradiction = true; ctx->done = true; break; } - // This could pass due to a global promoted const. - // So we need to add it to the dependencies on both branches. - _Py_BloomFilter_Add(dependencies, func); - if (sym_get_func_version(callable) == func_version) { - REPLACE_OP(this_instr, _NOP, 0, 0); - } - else { - sym_set_func_version(ctx, callable, func_version); - } + // Guarded on this, so it can be promoted. + sym_set_const(callable, func); + // We do not need to add func to the bloom filter, as we never remove + // this guard. Note: we generally do not want to add functions to our dependencies, + // as we want to avoid having to invalidate all executors on every function + // deallocation, which is a common procedure (e.g. lambdas). } op(_CHECK_METHOD_VERSION, (func_version/2, callable, null, unused[oparg] -- callable, null, unused[oparg])) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 7eb81455710118..6a896415f69cdb 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3620,18 +3620,13 @@ callable = stack_pointer[-2 - oparg]; uint32_t func_version = (uint32_t)this_instr->operand0; PyObject *func = sym_get_probable_value(callable); - if (func == NULL || !PyFunction_Check(func)) { + if (func == NULL || !PyFunction_Check(func) || ((PyFunctionObject *)func)->func_version != func_version) { ctx->contradiction = true; ctx->done = true; break; } + sym_set_const(callable, func); _Py_BloomFilter_Add(dependencies, func); - if (sym_get_func_version(callable) == func_version) { - REPLACE_OP(this_instr, _NOP, 0, 0); - } - else { - sym_set_func_version(ctx, callable, func_version); - } break; } diff --git a/Python/optimizer_symbols.c b/Python/optimizer_symbols.c index d0f33b80a570dd..79f81482d247e3 100644 --- a/Python/optimizer_symbols.c +++ b/Python/optimizer_symbols.c @@ -35,12 +35,12 @@ NULL | | RECORDED_VALUE* | TYPE_VERSION | | | | | | | | <- Anything below this level has a known type. | KNOWN_CLASS--+ | | | -| | | | | | PREDICATE RECORDED_VALUE(known type) -| | | INT* | | | | -| | | | | | | | <- Anything below this level has a known truthiness. -| | | | FUNC_VERSION | | | -| TUPLE | | | TRUTHINESS | | -| | | | | | | | <- Anything below this level is a known constant. +| | | | | PREDICATE RECORDED_VALUE(known type) +| | | INT* | | | +| | | | | | | <- Anything below this level has a known truthiness. +| | | | | | | +| TUPLE | | TRUTHINESS | | +| | | | | | | <- Anything below this level is a known constant. | KNOWN_VALUE--+-------+----+------+ | | <- Anything below this level is unreachable. BOTTOM @@ -101,9 +101,6 @@ _PyUOpSymPrint(JitOptRef ref) case JIT_SYM_TYPE_VERSION_TAG: printf("", sym->version.version, (void *)sym); break; - case JIT_SYM_FUNC_VERSION_TAG: - printf("", sym->func_version.func_version); - break; case JIT_SYM_KNOWN_CLASS_TAG: printf("<%s at %p>", sym->cls.type->tp_name, (void *)sym); break; @@ -325,11 +322,6 @@ _Py_uop_sym_set_type(JitOptContext *ctx, JitOptRef ref, PyTypeObject *typ) sym_set_bottom(ctx, sym); } return; - case JIT_SYM_FUNC_VERSION_TAG: - if (typ != &PyFunction_Type) { - sym_set_bottom(ctx, sym); - } - return; case JIT_SYM_KNOWN_VALUE_TAG: if (Py_TYPE(sym->value.value) != typ) { Py_CLEAR(sym->value.value); @@ -433,12 +425,6 @@ _Py_uop_sym_set_type_version(JitOptContext *ctx, JitOptRef ref, unsigned int ver return false; } return true; - case JIT_SYM_FUNC_VERSION_TAG: - if (version != PyFunction_Type.tp_version_tag) { - sym_set_bottom(ctx, sym); - return false; - } - return true; case JIT_SYM_BOTTOM_TAG: return false; case JIT_SYM_NON_NULL_TAG: @@ -486,87 +472,6 @@ _Py_uop_sym_set_type_version(JitOptContext *ctx, JitOptRef ref, unsigned int ver Py_UNREACHABLE(); } -bool -_Py_uop_sym_set_func_version(JitOptContext *ctx, JitOptRef ref, uint32_t version) -{ - JitOptSymbol *sym = PyJitRef_Unwrap(ref); - JitSymType tag = sym->tag; - switch(tag) { - case JIT_SYM_NULL_TAG: - sym_set_bottom(ctx, sym); - return false; - case JIT_SYM_KNOWN_CLASS_TAG: - if (sym->cls.type != &PyFunction_Type) { - sym_set_bottom(ctx, sym); - return false; - } - sym->tag = JIT_SYM_FUNC_VERSION_TAG; - sym->version.version = version; - return true; - case JIT_SYM_KNOWN_VALUE_TAG: - if (Py_TYPE(sym->value.value) != &PyFunction_Type || - ((PyFunctionObject *)sym->value.value)->func_version != version) { - Py_CLEAR(sym->value.value); - sym_set_bottom(ctx, sym); - return false; - } - return true; - case JIT_SYM_TYPE_VERSION_TAG: - if (sym->version.version != PyFunction_Type.tp_version_tag) { - sym_set_bottom(ctx, sym); - return false; - } - sym->tag = JIT_SYM_FUNC_VERSION_TAG; - sym->version.version = version; - return true; - case JIT_SYM_FUNC_VERSION_TAG: - if (sym->func_version.func_version != version) { - sym_set_bottom(ctx, sym); - return false; - } - return true; - case JIT_SYM_BOTTOM_TAG: - return false; - case JIT_SYM_NON_NULL_TAG: - case JIT_SYM_UNKNOWN_TAG: - sym->tag = JIT_SYM_FUNC_VERSION_TAG; - sym->func_version.func_version = version; - return true; - case JIT_SYM_RECORDED_GEN_FUNC_TAG: - case JIT_SYM_COMPACT_INT: - case JIT_SYM_TUPLE_TAG: - case JIT_SYM_PREDICATE_TAG: - case JIT_SYM_TRUTHINESS_TAG: - sym_set_bottom(ctx, sym); - return false; - case JIT_SYM_RECORDED_VALUE_TAG: { - PyObject *val = sym->recorded_value.value; - if (Py_TYPE(val) != &PyFunction_Type || - ((PyFunctionObject *)sym->recorded_value.value)->func_version != version) { - sym_set_bottom(ctx, sym); - return false; - } - // Promote to known value, as we have guarded/checked on it. - sym->tag = JIT_SYM_KNOWN_VALUE_TAG; - // New ownership. We need to NewRef here, as - // it's originally kept alive by the trace buffer. - sym->value.value = Py_NewRef(val); - return true; - } - case JIT_SYM_RECORDED_TYPE_TAG: - if (sym->recorded_type.type == &PyFunction_Type) { - sym->tag = JIT_SYM_FUNC_VERSION_TAG; - sym->func_version.func_version = version; - return true; - } - else { - sym_set_bottom(ctx, sym); - return false; - } - } - Py_UNREACHABLE(); -} - void _Py_uop_sym_set_const(JitOptContext *ctx, JitOptRef ref, PyObject *const_val) { @@ -612,14 +517,6 @@ _Py_uop_sym_set_const(JitOptContext *ctx, JitOptRef ref, PyObject *const_val) } make_const(sym, const_val); return; - case JIT_SYM_FUNC_VERSION_TAG: - if (Py_TYPE(const_val) != &PyFunction_Type || - ((PyFunctionObject *)const_val)->func_version != sym->func_version.func_version) { - sym_set_bottom(ctx, sym); - return; - } - make_const(sym, const_val); - return; case JIT_SYM_BOTTOM_TAG: return; case JIT_SYM_RECORDED_VALUE_TAG: @@ -796,8 +693,6 @@ _Py_uop_sym_get_type(JitOptRef ref) return Py_TYPE(sym->value.value); case JIT_SYM_TYPE_VERSION_TAG: return _PyType_LookupByVersion(sym->version.version); - case JIT_SYM_FUNC_VERSION_TAG: - return &PyFunction_Type; case JIT_SYM_TUPLE_TAG: return &PyTuple_Type; case JIT_SYM_PREDICATE_TAG: @@ -820,7 +715,6 @@ _Py_uop_sym_get_probable_type(JitOptRef ref) case JIT_SYM_NON_NULL_TAG: case JIT_SYM_UNKNOWN_TAG: case JIT_SYM_TYPE_VERSION_TAG: - case JIT_SYM_FUNC_VERSION_TAG: case JIT_SYM_TUPLE_TAG: case JIT_SYM_PREDICATE_TAG: case JIT_SYM_TRUTHINESS_TAG: @@ -853,8 +747,6 @@ _Py_uop_sym_get_type_version(JitOptRef ref) return 0; case JIT_SYM_TYPE_VERSION_TAG: return sym->version.version; - case JIT_SYM_FUNC_VERSION_TAG: - return PyFunction_Type.tp_version_tag; case JIT_SYM_KNOWN_CLASS_TAG: return sym->cls.version; case JIT_SYM_KNOWN_VALUE_TAG: @@ -872,37 +764,6 @@ _Py_uop_sym_get_type_version(JitOptRef ref) Py_UNREACHABLE(); } -uint32_t -_Py_uop_sym_get_func_version(JitOptRef ref) -{ - JitOptSymbol *sym = PyJitRef_Unwrap(ref); - JitSymType tag = sym->tag; - switch(tag) { - case JIT_SYM_NULL_TAG: - case JIT_SYM_BOTTOM_TAG: - case JIT_SYM_NON_NULL_TAG: - case JIT_SYM_UNKNOWN_TAG: - case JIT_SYM_RECORDED_VALUE_TAG: - case JIT_SYM_RECORDED_TYPE_TAG: - case JIT_SYM_TYPE_VERSION_TAG: - case JIT_SYM_KNOWN_CLASS_TAG: - case JIT_SYM_TUPLE_TAG: - case JIT_SYM_PREDICATE_TAG: - case JIT_SYM_TRUTHINESS_TAG: - case JIT_SYM_COMPACT_INT: - case JIT_SYM_RECORDED_GEN_FUNC_TAG: - return 0; - case JIT_SYM_FUNC_VERSION_TAG: - return sym->func_version.func_version; - case JIT_SYM_KNOWN_VALUE_TAG: - if (Py_TYPE(sym->value.value) == &PyFunction_Type) { - return ((PyFunctionObject *)sym->value.value)->func_version; - } - return 0; - } - Py_UNREACHABLE(); -} - bool _Py_uop_sym_has_type(JitOptRef sym) @@ -935,7 +796,6 @@ _Py_uop_sym_get_probable_value(JitOptRef ref) case JIT_SYM_UNKNOWN_TAG: case JIT_SYM_RECORDED_TYPE_TAG: case JIT_SYM_TYPE_VERSION_TAG: - case JIT_SYM_FUNC_VERSION_TAG: case JIT_SYM_TUPLE_TAG: case JIT_SYM_PREDICATE_TAG: case JIT_SYM_TRUTHINESS_TAG: @@ -1005,8 +865,6 @@ _Py_uop_sym_truthiness(JitOptContext *ctx, JitOptRef ref) return -1; case JIT_SYM_KNOWN_VALUE_TAG: break; - case JIT_SYM_FUNC_VERSION_TAG: - return 1; case JIT_SYM_TUPLE_TAG: return sym->tuple.length != 0; case JIT_SYM_TRUTHINESS_TAG: @@ -1155,7 +1013,6 @@ _Py_uop_sym_set_compact_int(JitOptContext *ctx, JitOptRef ref) sym_set_bottom(ctx, sym); } return; - case JIT_SYM_FUNC_VERSION_TAG: case JIT_SYM_TUPLE_TAG: case JIT_SYM_PREDICATE_TAG: case JIT_SYM_TRUTHINESS_TAG: @@ -1343,7 +1200,6 @@ _Py_uop_sym_set_recorded_value(JitOptContext *ctx, JitOptRef ref, PyObject *valu case JIT_SYM_PREDICATE_TAG: case JIT_SYM_TRUTHINESS_TAG: case JIT_SYM_COMPACT_INT: - case JIT_SYM_FUNC_VERSION_TAG: return; } Py_UNREACHABLE(); @@ -1367,9 +1223,6 @@ _Py_uop_sym_set_recorded_gen_func(JitOptContext *ctx, JitOptRef ref, PyFunctionO case JIT_SYM_PREDICATE_TAG: case JIT_SYM_TRUTHINESS_TAG: case JIT_SYM_COMPACT_INT: - case JIT_SYM_FUNC_VERSION_TAG: - sym_set_bottom(ctx, sym); - return; case JIT_SYM_BOTTOM_TAG: return; case JIT_SYM_NON_NULL_TAG: @@ -1464,7 +1317,6 @@ _Py_uop_sym_set_recorded_type(JitOptContext *ctx, JitOptRef ref, PyTypeObject *t case JIT_SYM_TRUTHINESS_TAG: case JIT_SYM_COMPACT_INT: case JIT_SYM_RECORDED_GEN_FUNC_TAG: - case JIT_SYM_FUNC_VERSION_TAG: return; } Py_UNREACHABLE(); @@ -2128,15 +1980,6 @@ _Py_uop_symbols_test(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(ignored)) TEST_PREDICATE(_Py_uop_sym_matches_type(ref_int, &PyLong_Type), "43 is not an int"); TEST_PREDICATE(_Py_uop_sym_get_const(ctx, ref_int) == val_43, "43 isn't 43"); - // Test func version's important transitions. - JitOptRef func_version = _Py_uop_sym_new_not_null(ctx); - TEST_PREDICATE(_Py_uop_sym_get_func_version(func_version) == 0, "func version should be unset"); - _Py_uop_sym_set_func_version(ctx, func_version, 172); - TEST_PREDICATE(_Py_uop_sym_get_func_version(func_version) == 172, "func version should be set"); - func_version = _Py_uop_sym_new_type(ctx, &PyFunction_Type); - _Py_uop_sym_set_func_version(ctx, func_version, 192); - TEST_PREDICATE(_Py_uop_sym_get_func_version(func_version) == 192, "func version should be set"); - // Test recorded values /* Test that recorded values aren't treated as known values*/ From 8806c74b0e172217138fa4e868a392f83945a403 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 13 Apr 2026 01:32:54 +0800 Subject: [PATCH 5/6] Fixups --- Lib/test/test_capi/test_opt.py | 18 ++++++++++++++++++ Python/optimizer_bytecodes.c | 6 ++++-- Python/optimizer_cases.c.h | 7 ++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 5eedbc3d27dc76..d8e9b16b472d6e 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -5174,6 +5174,24 @@ def g(): PYTHON_JIT="1", PYTHON_JIT_STRESS="1") self.assertEqual(result[0].rc, 0, result) + def test_func_version_guarded_on_change(self): + def testfunc(n): + for i in range(n): + # Only works on functions promoted to constants + global_identity_code_will_be_modified(i) + + testfunc(TIER2_THRESHOLD) + + ex = get_first_executor(testfunc) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_PUSH_FRAME", uops) + self.assertIn("_CHECK_FUNCTION_VERSION", uops) + + global_identity_code_will_be_modified.__code__ = (lambda a: 0xdeadead).__code__ + # JItted code should've deopted. + self.assertEqual(global_identity_code_will_be_modified(None), 0xdeadead) + def test_call_super(self): class A: def method1(self): diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 297c5285e48b79..123644fef8f361 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -2235,7 +2235,8 @@ dummy_func(void) { if (co->co_version == version) { _Py_BloomFilter_Add(dependencies, co); // Functions derive their version from code objects. - if (sym_get_func_version(ctx->frame->callable) == version) { + PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable); + if (func != NULL && func->func_version == version) { REPLACE_OP(this_instr, _NOP, 0, 0); } } @@ -2268,7 +2269,8 @@ dummy_func(void) { op(_GUARD_IP__PUSH_FRAME, (ip/4 --)) { (void)ip; stack_pointer = sym_set_stack_depth((int)this_instr->operand1, stack_pointer); - if (sym_get_func_version(ctx->frame->callable) != 0 && + PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable); + if (func != NULL && func->func_version != 0 && // We can remove this guard for simple function call targets. (((PyCodeObject *)ctx->frame->func->func_code)->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) == 0) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 6a896415f69cdb..40cf0bce675635 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3626,7 +3626,6 @@ break; } sym_set_const(callable, func); - _Py_BloomFilter_Add(dependencies, func); break; } @@ -5059,7 +5058,8 @@ PyCodeObject *co = get_current_code_object(ctx); if (co->co_version == version) { _Py_BloomFilter_Add(dependencies, co); - if (sym_get_func_version(ctx->frame->callable) == version) { + PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable); + if (func != NULL && func->func_version == version) { REPLACE_OP(this_instr, _NOP, 0, 0); } } @@ -5100,7 +5100,8 @@ PyObject *ip = (PyObject *)this_instr->operand0; (void)ip; stack_pointer = sym_set_stack_depth((int)this_instr->operand1, stack_pointer); - if (sym_get_func_version(ctx->frame->callable) != 0 && + PyFunctionObject *func = (PyFunctionObject *)sym_get_const(ctx, ctx->frame->callable); + if (func != NULL && func->func_version != 0 && // We can remove this guard for simple function call targets. (((PyCodeObject *)ctx->frame->func->func_code)->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) == 0) { From 6a42001dc9e97ca62ecb5965ed028b1284e194f4 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Mon, 13 Apr 2026 01:46:41 +0800 Subject: [PATCH 6/6] Add back test properly --- Lib/test/test_capi/test_opt.py | 3 +++ Objects/funcobject.c | 7 +++++++ Python/optimizer_bytecodes.c | 5 +---- Python/optimizer_cases.c.h | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index d8e9b16b472d6e..bc2114c9a85493 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -5189,6 +5189,9 @@ def testfunc(n): self.assertIn("_CHECK_FUNCTION_VERSION", uops) global_identity_code_will_be_modified.__code__ = (lambda a: 0xdeadead).__code__ + _testinternalcapi.clear_executor_deletion_list() + ex = get_first_executor(testfunc) + self.assertIsNone(ex) # JItted code should've deopted. self.assertEqual(global_identity_code_will_be_modified(None), 0xdeadead) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 94f102ff9329cb..0fffd36ad462da 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -64,6 +64,13 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func, case PyFunction_EVENT_MODIFY_DEFAULTS: case PyFunction_EVENT_MODIFY_KWDEFAULTS: case PyFunction_EVENT_MODIFY_QUALNAME: +#if _Py_TIER2 + // Note: we only invalidate JIT code if a function version changes. + // Not when the function is deallocated. + // Function deallocation occurs frequently (think: lambdas), + // so we want to minimize dependency invalidation there. + _Py_Executors_InvalidateDependency(interp, func, 1); +#endif RARE_EVENT_INTERP_INC(interp, func_modification); break; default: diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 123644fef8f361..fd1dfa017de4c4 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -1009,10 +1009,7 @@ dummy_func(void) { } // Guarded on this, so it can be promoted. sym_set_const(callable, func); - // We do not need to add func to the bloom filter, as we never remove - // this guard. Note: we generally do not want to add functions to our dependencies, - // as we want to avoid having to invalidate all executors on every function - // deallocation, which is a common procedure (e.g. lambdas). + _Py_BloomFilter_Add(dependencies, func); } op(_CHECK_METHOD_VERSION, (func_version/2, callable, null, unused[oparg] -- callable, null, unused[oparg])) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 40cf0bce675635..dc2d5891f9c3e1 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3626,6 +3626,7 @@ break; } sym_set_const(callable, func); + _Py_BloomFilter_Add(dependencies, func); break; }