From e8740df7dc14a2fb22af852c52a6e2d0a14a2b80 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Sun, 12 Apr 2026 11:43:44 -0400 Subject: [PATCH 1/4] gh-148438: implement `_RECORD_BOUND_METHOD` in JIT --- Lib/test/test_capi/test_opt.py | 45 ++++++++++++++++++++++++++++++++++ Python/bytecodes.c | 3 +-- Python/optimizer_bytecodes.c | 39 ++++++++++++++++++++++++++++- Python/optimizer_cases.c.h | 41 ++++++++++++++++++++++++++++++- Python/record_functions.c.h | 3 +-- 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 2678a620763b4b..a23fe73726bf3e 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -1691,6 +1691,51 @@ def testfunc(n): self.assertIn("_CHECK_FUNCTION_VERSION_INLINE", uops) self.assertNotIn("_CHECK_METHOD_VERSION", uops) + def test_record_bound_method_general(self): + class MyClass: + def method(self, *args): + return args[0] + 1 + + def testfunc(n): + obj = MyClass() + bound = obj.method + result = 0 + for i in range(n): + result += bound(i) + return result + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual( + res, sum(i + 1 for i in range(TIER2_THRESHOLD)) + ) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_PUSH_FRAME", uops) + self.assertIn("_CHECK_FUNCTION_VERSION_INLINE", uops) + self.assertNotIn("_CHECK_METHOD_VERSION", uops) + + def test_record_bound_method_exact_args(self): + class MyClass: + def method(self, x): + return x + 1 + + def testfunc(n): + obj = MyClass() + bound = obj.method + result = 0 + for i in range(n): + result += bound(i) + return result + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual( + res, sum(i + 1 for i in range(TIER2_THRESHOLD)) + ) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_PUSH_FRAME", uops) + self.assertNotIn("_CHECK_FUNCTION_EXACT_ARGS", uops) + def test_jit_error_pops(self): """ Tests that the correct number of pops are inserted into the diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 34e3885a93c8bd..63c3012bd34681 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -6151,8 +6151,7 @@ dummy_func( tier2 op(_RECORD_BOUND_METHOD, (callable, self, args[oparg] -- callable, self, args[oparg])) { PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); if (Py_TYPE(callable_o) == &PyMethod_Type) { - PyObject *func = ((PyMethodObject *)callable_o)->im_func; - RECORD_VALUE(func); + RECORD_VALUE(callable_o); } } diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 6e5af4793419f2..c0ea39e2bf6fe8 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -996,8 +996,16 @@ dummy_func(void) { } op(_INIT_CALL_BOUND_METHOD_EXACT_ARGS, (callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { - callable = sym_new_not_null(ctx); + PyObject *bound_method = sym_get_probable_value(callable); self_or_null = sym_new_not_null(ctx); + if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { + PyMethodObject *method = (PyMethodObject *)bound_method; + callable = sym_new_not_null(ctx); + sym_set_recorded_value(callable, method->im_func); + } + else { + callable = sym_new_not_null(ctx); + } } op(_CHECK_FUNCTION_VERSION, (func_version/2, callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { @@ -1016,6 +1024,19 @@ dummy_func(void) { ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)method->im_func; } + else { + PyObject *bound_method = sym_get_probable_value(callable); + if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { + PyMethodObject *method = (PyMethodObject *)bound_method; + PyObject *func = method->im_func; + if (PyFunction_Check(func) && + ((PyFunctionObject *)func)->func_version == func_version) { + sym_set_const(callable, bound_method); + ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); + uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)func; + } + } + } sym_set_type(callable, &PyMethod_Type); } @@ -1054,6 +1075,18 @@ dummy_func(void) { } } + op(_EXPAND_METHOD, (callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { + if (sym_is_const(ctx, callable) && sym_matches_type(callable, &PyMethod_Type)) { + PyMethodObject *method = (PyMethodObject *)sym_get_const(ctx, callable); + callable = sym_new_const(ctx, method->im_func); + self_or_null = sym_new_const(ctx, method->im_self); + } + else { + callable = sym_new_not_null(ctx); + self_or_null = sym_new_not_null(ctx); + } + } + op(_MAYBE_EXPAND_METHOD, (callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) { (void)args; callable = sym_new_not_null(ctx); @@ -2212,6 +2245,10 @@ dummy_func(void) { sym_set_recorded_value(func, (PyObject *)this_instr->operand0); } + op(_RECORD_BOUND_METHOD, (callable, self, args[oparg] -- callable, self, args[oparg])) { + sym_set_recorded_value(callable, (PyObject *)this_instr->operand0); + } + op(_RECORD_NOS_GEN_FUNC, (nos, tos -- nos, tos)) { PyFunctionObject *func = (PyFunctionObject *)this_instr->operand0; assert(func == NULL || PyFunction_Check(func)); diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index d90ad285a11f33..fa915dc90e2e20 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3642,11 +3642,39 @@ ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)method->im_func; } + else { + PyObject *bound_method = sym_get_probable_value(callable); + if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { + PyMethodObject *method = (PyMethodObject *)bound_method; + PyObject *func = method->im_func; + if (PyFunction_Check(func) && + ((PyFunctionObject *)func)->func_version == func_version) { + sym_set_const(callable, bound_method); + ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); + uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)func; + } + } + } sym_set_type(callable, &PyMethod_Type); break; } case _EXPAND_METHOD: { + JitOptRef self_or_null; + JitOptRef callable; + self_or_null = stack_pointer[-1 - oparg]; + callable = stack_pointer[-2 - oparg]; + if (sym_is_const(ctx, callable) && sym_matches_type(callable, &PyMethod_Type)) { + PyMethodObject *method = (PyMethodObject *)sym_get_const(ctx, callable); + callable = sym_new_const(ctx, method->im_func); + self_or_null = sym_new_const(ctx, method->im_self); + } + else { + callable = sym_new_not_null(ctx); + self_or_null = sym_new_not_null(ctx); + } + stack_pointer[-2 - oparg] = callable; + stack_pointer[-1 - oparg] = self_or_null; break; } @@ -3685,8 +3713,16 @@ JitOptRef callable; self_or_null = stack_pointer[-1 - oparg]; callable = stack_pointer[-2 - oparg]; - callable = sym_new_not_null(ctx); + PyObject *bound_method = sym_get_probable_value(callable); self_or_null = sym_new_not_null(ctx); + if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { + PyMethodObject *method = (PyMethodObject *)bound_method; + callable = sym_new_not_null(ctx); + sym_set_recorded_value(callable, method->im_func); + } + else { + callable = sym_new_not_null(ctx); + } stack_pointer[-2 - oparg] = callable; stack_pointer[-1 - oparg] = self_or_null; break; @@ -5192,6 +5228,9 @@ } case _RECORD_BOUND_METHOD: { + JitOptRef callable; + callable = stack_pointer[-2 - oparg]; + sym_set_recorded_value(callable, (PyObject *)this_instr->operand0); break; } diff --git a/Python/record_functions.c.h b/Python/record_functions.c.h index 02b8538bc902b5..2c89e3d4dfa6da 100644 --- a/Python/record_functions.c.h +++ b/Python/record_functions.c.h @@ -74,8 +74,7 @@ void _PyOpcode_RecordFunction_BOUND_METHOD(_PyInterpreterFrame *frame, _PyStackR callable = stack_pointer[-2 - oparg]; PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); if (Py_TYPE(callable_o) == &PyMethod_Type) { - PyObject *func = ((PyMethodObject *)callable_o)->im_func; - *recorded_value = (PyObject *)func; + *recorded_value = (PyObject *)callable_o; Py_INCREF(*recorded_value); } } From 271a7dc856dd54d5d556fff158b4e85f643ff8ae Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Sun, 12 Apr 2026 12:38:54 -0400 Subject: [PATCH 2/4] gh-148438: implement `_RECORD_BOUND_METHOD` in JIT --- Lib/test/test_capi/test_opt.py | 2 -- Python/optimizer_bytecodes.c | 2 -- Python/optimizer_cases.c.h | 2 -- 3 files changed, 6 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index a23fe73726bf3e..3100f16259e36b 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -1711,8 +1711,6 @@ def testfunc(n): self.assertIsNotNone(ex) uops = get_opnames(ex) self.assertIn("_PUSH_FRAME", uops) - self.assertIn("_CHECK_FUNCTION_VERSION_INLINE", uops) - self.assertNotIn("_CHECK_METHOD_VERSION", uops) def test_record_bound_method_exact_args(self): class MyClass: diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index c0ea39e2bf6fe8..3e8f02ee04b3f1 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -1032,8 +1032,6 @@ dummy_func(void) { if (PyFunction_Check(func) && ((PyFunctionObject *)func)->func_version == func_version) { sym_set_const(callable, bound_method); - ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); - uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)func; } } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index fa915dc90e2e20..e2644f5577f095 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3650,8 +3650,6 @@ if (PyFunction_Check(func) && ((PyFunctionObject *)func)->func_version == func_version) { sym_set_const(callable, bound_method); - ADD_OP(_CHECK_FUNCTION_VERSION_INLINE, 0, func_version); - uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)func; } } } From 420719d2c018257ed46dffc5011b59c98f2bedd5 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Sun, 12 Apr 2026 14:01:09 -0400 Subject: [PATCH 3/4] gh-148438: implement `_RECORD_BOUND_METHOD` in JIT --- Python/optimizer_bytecodes.c | 12 +++++++++++- Python/optimizer_cases.c.h | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 3e8f02ee04b3f1..26c969cee535a6 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -997,18 +997,26 @@ dummy_func(void) { op(_INIT_CALL_BOUND_METHOD_EXACT_ARGS, (callable, self_or_null, unused[oparg] -- callable, self_or_null, unused[oparg])) { PyObject *bound_method = sym_get_probable_value(callable); - self_or_null = sym_new_not_null(ctx); if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { PyMethodObject *method = (PyMethodObject *)bound_method; callable = sym_new_not_null(ctx); sym_set_recorded_value(callable, method->im_func); + self_or_null = sym_new_not_null(ctx); + sym_set_recorded_value(self_or_null, method->im_self); } else { callable = sym_new_not_null(ctx); + self_or_null = sym_new_not_null(ctx); } } 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) && + ((PyFunctionObject *)func)->func_version == func_version) { + _Py_BloomFilter_Add(dependencies, func); + sym_set_const(callable, func); + } if (sym_get_func_version(callable) == func_version) { REPLACE_OP(this_instr, _NOP, 0, 0); } @@ -1025,12 +1033,14 @@ dummy_func(void) { uop_buffer_last(&ctx->out_buffer)->operand1 = (uintptr_t)method->im_func; } else { + // Guarding on the bound method, safe to promote. PyObject *bound_method = sym_get_probable_value(callable); if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { PyMethodObject *method = (PyMethodObject *)bound_method; PyObject *func = method->im_func; if (PyFunction_Check(func) && ((PyFunctionObject *)func)->func_version == func_version) { + _Py_BloomFilter_Add(dependencies, func); sym_set_const(callable, bound_method); } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index e2644f5577f095..8bc478ff6b1cca 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3619,6 +3619,12 @@ 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) && + ((PyFunctionObject *)func)->func_version == func_version) { + _Py_BloomFilter_Add(dependencies, func); + sym_set_const(callable, func); + } if (sym_get_func_version(callable) == func_version) { REPLACE_OP(this_instr, _NOP, 0, 0); } @@ -3649,6 +3655,7 @@ PyObject *func = method->im_func; if (PyFunction_Check(func) && ((PyFunctionObject *)func)->func_version == func_version) { + _Py_BloomFilter_Add(dependencies, func); sym_set_const(callable, bound_method); } } @@ -3712,14 +3719,16 @@ self_or_null = stack_pointer[-1 - oparg]; callable = stack_pointer[-2 - oparg]; PyObject *bound_method = sym_get_probable_value(callable); - self_or_null = sym_new_not_null(ctx); if (bound_method != NULL && Py_TYPE(bound_method) == &PyMethod_Type) { PyMethodObject *method = (PyMethodObject *)bound_method; callable = sym_new_not_null(ctx); sym_set_recorded_value(callable, method->im_func); + self_or_null = sym_new_not_null(ctx); + sym_set_recorded_value(self_or_null, method->im_self); } else { callable = sym_new_not_null(ctx); + self_or_null = sym_new_not_null(ctx); } stack_pointer[-2 - oparg] = callable; stack_pointer[-1 - oparg] = self_or_null; From 84dda8bca6cf1aeb57bc9dedf1371643f6bb8380 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Sun, 12 Apr 2026 14:07:40 -0400 Subject: [PATCH 4/4] gh-148438: implement `_RECORD_BOUND_METHOD` in JIT --- Python/optimizer_bytecodes.c | 6 ------ Python/optimizer_cases.c.h | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 26c969cee535a6..dfae1b94543b31 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -1011,12 +1011,6 @@ 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) && - ((PyFunctionObject *)func)->func_version == func_version) { - _Py_BloomFilter_Add(dependencies, func); - sym_set_const(callable, 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 8bc478ff6b1cca..4dd62ea22d6b60 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -3619,12 +3619,6 @@ 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) && - ((PyFunctionObject *)func)->func_version == func_version) { - _Py_BloomFilter_Add(dependencies, func); - sym_set_const(callable, func); - } if (sym_get_func_version(callable) == func_version) { REPLACE_OP(this_instr, _NOP, 0, 0); }