]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148438: implement `_RECORD_BOUND_METHOD` in JIT (GH-148457)
authorNeko Asakura <neko.asakura@outlook.com>
Sun, 12 Apr 2026 18:57:55 +0000 (14:57 -0400)
committerGitHub <noreply@github.com>
Sun, 12 Apr 2026 18:57:55 +0000 (02:57 +0800)
Lib/test/test_capi/test_opt.py
Python/bytecodes.c
Python/optimizer_bytecodes.c
Python/optimizer_cases.c.h
Python/record_functions.c.h

index 4a26bbc96ce9a4331ba0766970d950123ec0c622..03ec53b93a3ba9ae629b1835d3aadc6b3ff685f0 100644 (file)
@@ -1723,6 +1723,49 @@ class TestUopsOptimization(unittest.TestCase):
         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)
+
+    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
index 351a50b5d537877653e4aa7c48291dc79cae66a7..a274fbf7403af61661cf9ce1ff5017a8256cdc73 100644 (file)
@@ -6160,8 +6160,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);
             }
         }
 
index ab9d23aef5eacf05d0dcdfc0beffb1be298d7bbd..957575dcfaccce7290d5a5af46999172de90509d 100644 (file)
@@ -996,8 +996,18 @@ 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);
-        self_or_null = sym_new_not_null(ctx);
+        PyObject *bound_method = sym_get_probable_value(callable);
+        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])) {
@@ -1019,6 +1029,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 {
+            // 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);
+                }
+            }
+        }
         sym_set_type(callable, &PyMethod_Type);
     }
 
@@ -1057,6 +1080,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);
@@ -2226,6 +2261,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));
index b9895a63f214dcc3bda0383c2603e4fecabcd186..553d819fcdeefb464bbe6224b6e021c38c6619d5 100644 (file)
                 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) {
+                        _Py_BloomFilter_Add(dependencies, func);
+                        sym_set_const(callable, bound_method);
+                    }
+                }
+            }
             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;
         }
 
             JitOptRef callable;
             self_or_null = stack_pointer[-1 - oparg];
             callable = stack_pointer[-2 - oparg];
-            callable = sym_new_not_null(ctx);
-            self_or_null = sym_new_not_null(ctx);
+            PyObject *bound_method = sym_get_probable_value(callable);
+            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;
             break;
         }
 
         case _RECORD_BOUND_METHOD: {
+            JitOptRef callable;
+            callable = stack_pointer[-2 - oparg];
+            sym_set_recorded_value(callable, (PyObject *)this_instr->operand0);
             break;
         }
 
index 02b8538bc902b56684b6b1cd20cb346573267879..2c89e3d4dfa6dafe868d498a837893e571236a9d 100644 (file)
@@ -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);
     }
 }