From e9a08b060eebebe66ef1d6a904978b5b25cf3456 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 10 Apr 2026 18:45:13 +0530 Subject: [PATCH] constant fold classmethod and staticmethod in JIT --- Include/internal/pycore_opcode_metadata.h | 4 +- Lib/test/test_capi/test_opt.py | 14 ++++++- Python/bytecodes.c | 1 + Python/optimizer_bytecodes.c | 40 ++++++++++++++++-- Python/optimizer_cases.c.h | 51 +++++++++++++++++++---- Python/record_functions.c.h | 1 + Tools/cases_generator/analyzer.py | 3 +- 7 files changed, 98 insertions(+), 16 deletions(-) diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h index 79ca8a1bfa88c0..9c4364cfcb3200 100644 --- a/Include/internal/pycore_opcode_metadata.h +++ b/Include/internal/pycore_opcode_metadata.h @@ -1220,7 +1220,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = { [JUMP_FORWARD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG }, [LIST_APPEND] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG }, [LIST_EXTEND] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG }, - [LOAD_ATTR] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG }, + [LOAD_ATTR] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [LOAD_ATTR_CLASS] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG }, [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_RECORDS_VALUE_FLAG }, [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_SYNC_SP_FLAG | HAS_NEEDS_GUARD_IP_FLAG }, @@ -1441,7 +1441,7 @@ _PyOpcode_macro_expansion[256] = { [JUMP_BACKWARD_NO_JIT] = { .nuops = 2, .uops = { { _CHECK_PERIODIC, OPARG_SIMPLE, 1 }, { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 1 } } }, [LIST_APPEND] = { .nuops = 1, .uops = { { _LIST_APPEND, OPARG_SIMPLE, 0 } } }, [LIST_EXTEND] = { .nuops = 2, .uops = { { _LIST_EXTEND, OPARG_SIMPLE, 0 }, { _POP_TOP, OPARG_SIMPLE, 0 } } }, - [LOAD_ATTR] = { .nuops = 1, .uops = { { _LOAD_ATTR, OPARG_SIMPLE, 8 } } }, + [LOAD_ATTR] = { .nuops = 2, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 0 }, { _LOAD_ATTR, OPARG_SIMPLE, 8 } } }, [LOAD_ATTR_CLASS] = { .nuops = 3, .uops = { { _CHECK_ATTR_CLASS, 2, 1 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { .nuops = 5, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_ATTR_CLASS, 2, 3 }, { _LOAD_ATTR_CLASS, 4, 5 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, [LOAD_ATTR_INSTANCE_VALUE] = { .nuops = 6, .uops = { { _RECORD_TOS_TYPE, OPARG_SIMPLE, 1 }, { _GUARD_TYPE_VERSION, 2, 1 }, { _CHECK_MANAGED_OBJECT_HAS_VALUES, OPARG_SIMPLE, 3 }, { _LOAD_ATTR_INSTANCE_VALUE, 1, 3 }, { _POP_TOP, OPARG_SIMPLE, 4 }, { _PUSH_NULL_CONDITIONAL, OPARG_SIMPLE, 9 } } }, diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 2c6584592b81ee..192a9d9af3940e 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -3173,11 +3173,20 @@ def m(self): class E(Exception): def m(self): return 1 + class F: + @classmethod + def class_method(cls): + return 1 + @staticmethod + def static_method(): + return 1 + def f(n): x = 0 c = C() d = D() e = E() + f = F() for _ in range(n): x += C.A # _LOAD_ATTR_CLASS x += c.A # _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES @@ -3185,12 +3194,15 @@ def f(n): x += c.m() # _LOAD_ATTR_METHOD_WITH_VALUES x += d.m() # _LOAD_ATTR_METHOD_NO_DICT x += e.m() # _LOAD_ATTR_METHOD_LAZY_DICT + x += f.class_method() # _LOAD_ATTR + x += f.static_method() # _LOAD_ATTR return x res, ex = self._run_with_optimizer(f, TIER2_THRESHOLD) - self.assertEqual(res, 6 * TIER2_THRESHOLD) + self.assertEqual(res, 8 * TIER2_THRESHOLD) self.assertIsNotNone(ex) uops = get_opnames(ex) + self.assertNotIn("_LOAD_ATTR", uops) self.assertNotIn("_LOAD_ATTR_CLASS", uops) self.assertNotIn("_LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES", uops) self.assertNotIn("_LOAD_ATTR_NONDESCRIPTOR_NO_DICT", uops) diff --git a/Python/bytecodes.c b/Python/bytecodes.c index bd7228350be678..59b637c0f90387 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2728,6 +2728,7 @@ dummy_func( macro(LOAD_ATTR) = _SPECIALIZE_LOAD_ATTR + + _RECORD_TOS_TYPE + unused/8 + _LOAD_ATTR; diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index e95ccd4d448e18..49c20f6c8adec6 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -873,10 +873,42 @@ dummy_func(void) { } op(_LOAD_ATTR, (owner -- attr, self_or_null[oparg&1])) { - (void)owner; - attr = sym_new_not_null(ctx); - if (oparg & 1) { - self_or_null[0] = sym_new_unknown(ctx); + PyTypeObject *type = sym_get_probable_type(owner); + if (oparg & 1 && type != NULL) { + PyObject *name = get_co_name(ctx, oparg >> 1); + PyObject *descr = _PyType_Lookup(type, name); + bool class_method = descr && Py_IS_TYPE(descr, &PyClassMethod_Type); + bool static_method = descr && Py_IS_TYPE(descr, &PyStaticMethod_Type); + if (class_method || static_method) { + PyObject *callable = NULL; + if (class_method) { + callable = _PyClassMethod_GetFunc(descr); + } + else { + assert(static_method); + callable = _PyStaticMethod_GetFunc(descr); + } + assert(callable); + bool immortal = _Py_IsImmortal(callable) || (type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE); + ADD_OP(_GUARD_TYPE_VERSION, 0, type->tp_version_tag); + ADD_OP(_POP_TOP, 0, 0); + ADD_OP(immortal ? _LOAD_CONST_INLINE_BORROW : _LOAD_CONST_INLINE, 0, (uintptr_t)callable); + if (class_method) { + ADD_OP(_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)type); + self_or_null[0] = sym_new_const(ctx, (PyObject *)type); + } else if (static_method) { + ADD_OP(_PUSH_NULL, 0, 0); + self_or_null[0] = sym_new_null(ctx); + } + attr = sym_new_const(ctx, callable); + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); + _Py_BloomFilter_Add(dependencies, (PyTypeObject *)type); + } else { + attr = sym_new_not_null(ctx); + self_or_null[0] = sym_new_unknown(ctx); + } + } else { + attr = sym_new_not_null(ctx); } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index e8a7ec216b040b..90db2783037dfe 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2409,15 +2409,50 @@ JitOptRef *self_or_null; owner = stack_pointer[-1]; self_or_null = &stack_pointer[0]; - (void)owner; - attr = sym_new_not_null(ctx); - if (oparg & 1) { - self_or_null[0] = sym_new_unknown(ctx); + PyTypeObject *type = sym_get_probable_type(owner); + if (oparg & 1 && type != NULL) { + PyObject *name = get_co_name(ctx, oparg >> 1); + PyObject *descr = _PyType_Lookup(type, name); + bool class_method = descr && Py_IS_TYPE(descr, &PyClassMethod_Type); + bool static_method = descr && Py_IS_TYPE(descr, &PyStaticMethod_Type); + if (class_method || static_method) { + PyObject *callable = NULL; + if (class_method) { + callable = _PyClassMethod_GetFunc(descr); + } + else { + assert(static_method); + callable = _PyStaticMethod_GetFunc(descr); + } + assert(callable); + bool immortal = _Py_IsImmortal(callable) || (type->tp_flags & Py_TPFLAGS_IMMUTABLETYPE); + ADD_OP(_GUARD_TYPE_VERSION, 0, type->tp_version_tag); + ADD_OP(_POP_TOP, 0, 0); + ADD_OP(immortal ? _LOAD_CONST_INLINE_BORROW : _LOAD_CONST_INLINE, 0, (uintptr_t)callable); + if (class_method) { + ADD_OP(_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)type); + self_or_null[0] = sym_new_const(ctx, (PyObject *)type); + } else if (static_method) { + ADD_OP(_PUSH_NULL, 0, 0); + self_or_null[0] = sym_new_null(ctx); + } + attr = sym_new_const(ctx, callable); + CHECK_STACK_BOUNDS((oparg&1)); + stack_pointer[-1] = attr; + stack_pointer += (oparg&1); + ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + PyType_Watch(TYPE_WATCHER_ID, (PyObject *)type); + _Py_BloomFilter_Add(dependencies, (PyTypeObject *)type); + } else { + attr = sym_new_not_null(ctx); + self_or_null[0] = sym_new_unknown(ctx); + stack_pointer += (oparg&1); + } + } else { + attr = sym_new_not_null(ctx); + stack_pointer += (oparg&1); } - CHECK_STACK_BOUNDS((oparg&1)); - stack_pointer[-1] = attr; - stack_pointer += (oparg&1); - ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); + stack_pointer[-1 - (oparg&1)] = attr; break; } diff --git a/Python/record_functions.c.h b/Python/record_functions.c.h index 33fd2f19b1230d..38a503cc03f2ec 100644 --- a/Python/record_functions.c.h +++ b/Python/record_functions.c.h @@ -97,6 +97,7 @@ const uint8_t _PyOpcode_RecordFunctionIndices[256] = { [BINARY_OP_SUBSCR_GETITEM] = _RECORD_NOS_INDEX, [SEND_GEN] = _RECORD_3OS_GEN_FUNC_INDEX, [LOAD_SUPER_ATTR_METHOD] = _RECORD_NOS_INDEX, + [LOAD_ATTR] = _RECORD_TOS_TYPE_INDEX, [LOAD_ATTR_INSTANCE_VALUE] = _RECORD_TOS_TYPE_INDEX, [LOAD_ATTR_WITH_HINT] = _RECORD_TOS_TYPE_INDEX, [LOAD_ATTR_SLOT] = _RECORD_TOS_TYPE_INDEX, diff --git a/Tools/cases_generator/analyzer.py b/Tools/cases_generator/analyzer.py index 6ba9c43ef1f0c3..de6538b8d26462 100644 --- a/Tools/cases_generator/analyzer.py +++ b/Tools/cases_generator/analyzer.py @@ -1149,7 +1149,8 @@ def add_macro( f"Recording uop {part.name} must be first in macro", macro.tokens[0]) parts.append(uop) - first = False + if uop.properties.tier != 1: + first = False case parser.CacheEffect(): parts.append(Skip(part.size)) case _: