diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 7be1c9eebb3bf9..0409a0709048c8 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2191,17 +2191,154 @@ def testfunc(n): self.assertNotIn("_TO_BOOL_BOOL", uops) self.assertIn("_GUARD_IS_TRUE_POP", uops) - def test_call_isinstance_tuple_of_classes(self): + def test_call_isinstance_tuple_of_classes_is_true(self): def testfunc(n): x = 0 for _ in range(n): - # A tuple of classes is currently not optimized, - # so this is only narrowed to bool: y = isinstance(42, (int, str)) if y: x += 1 return x + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertNotIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertNotIn("_GUARD_IS_TRUE_POP", uops) + self.assertIn("_BUILD_TUPLE", uops) + self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) + + def test_call_isinstance_tuple_of_classes_is_false(self): + def testfunc(n): + x = 0 + for _ in range(n): + y = isinstance(42, (bool, str)) + if not y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertNotIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertNotIn("_GUARD_IS_FALSE_POP", uops) + self.assertIn("_BUILD_TUPLE", uops) + self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) + + def test_call_isinstance_tuple_of_classes_true_unknown_1(self): + def testfunc(n): + x = 0 + for _ in range(n): + # One of the classes is unknown, but it comes + # after a known class, so we can narrow to True and + # remove the isinstance call. + y = isinstance(42, (int, eval('str'))) + if y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertNotIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertNotIn("_GUARD_IS_TRUE_POP", uops) + self.assertIn("_BUILD_TUPLE", uops) + self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) + + def test_call_isinstance_tuple_of_classes_true_unknown_2(self): + def testfunc(n): + x = 0 + for _ in range(n): + # We can narrow to True, but since the unknown class comes + # first and could potentially trigger an __instancecheck__, + # we can't remove the isinstance call. + y = isinstance(42, (eval('str'), int)) + if y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertNotIn("_GUARD_IS_TRUE_POP", uops) + + def test_call_isinstance_tuple_of_classes_true_unknown_3(self): + def testfunc(n): + x = 0 + for _ in range(n): + # We can only narrow to bool here + y = isinstance(42, (str, eval('int'))) + if y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertIn("_GUARD_IS_TRUE_POP", uops) + + def test_call_isinstance_tuple_of_classes_true_unknown_4(self): + def testfunc(n): + x = 0 + for _ in range(n): + # We can only narrow to bool here + y = isinstance(42, (eval('int'), str)) + if y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertIn("_GUARD_IS_TRUE_POP", uops) + + def test_call_isinstance_empty_tuple(self): + def testfunc(n): + x = 0 + for _ in range(n): + y = isinstance(42, ()) + if not y: + x += 1 + return x + + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertIsNotNone(ex) + uops = get_opnames(ex) + self.assertNotIn("_CALL_ISINSTANCE", uops) + self.assertNotIn("_TO_BOOL_BOOL", uops) + self.assertNotIn("_GUARD_IS_FALSE_POP", uops) + self.assertNotIn("_POP_TOP_LOAD_CONST_INLINE_BORROW", uops) + self.assertNotIn("_POP_CALL_LOAD_CONST_INLINE_BORROW", uops) + self.assertNotIn("_POP_CALL_ONE_LOAD_CONST_INLINE_BORROW", uops) + self.assertNotIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) + + def test_call_isinstance_tuple_unknown_length(self): + def testfunc(n): + x = 0 + for _ in range(n): + # tuple with an unknown length, we only narrow to bool + tup = tuple(eval('(int, str)')) + y = isinstance(42, tup) + if y: + x += 1 + return x + res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) self.assertEqual(res, TIER2_THRESHOLD) self.assertIsNotNone(ex) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-22-22-19-56.gh-issue-131798.kxRt1-.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-22-22-19-56.gh-issue-131798.kxRt1-.rst new file mode 100644 index 00000000000000..66295ebecdf18f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-22-22-19-56.gh-issue-131798.kxRt1-.rst @@ -0,0 +1,2 @@ +Optimize ``_CALL_ISINSTANCE`` in the JIT when the second argument is a tuple +of classes. diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index aeff76affd8ace..5f80ded47b1de0 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -956,7 +956,6 @@ dummy_func(void) { // isinstance(inst, cls) where both inst and cls have // known types, meaning we can deduce either True or False - // The below check is equivalent to PyObject_TypeCheck(inst, cls) PyObject *out = Py_False; if (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o)) { out = Py_True; @@ -964,6 +963,47 @@ dummy_func(void) { sym_set_const(res, out); REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); } + else if (inst_type && sym_matches_type(cls, &PyTuple_Type)) { + // isinstance(inst, tup) where inst has a known type and tup is a tuple. + // We can deduce True if inst is an instance of at least one of + // the items in the tuple. + // We can deduce False if all items in the tuple have known types and + // inst is not an instance of any of them. + + int length = sym_tuple_length(cls); + if (length != -1) { + // We cannot do anything about tuples with unknown length + bool can_replace_op = true; + PyObject *out = Py_False; + for (int i = 0; i < length; i++) { + JitOptRef item = sym_tuple_getitem(ctx, cls, i); + if (!sym_has_type(item)) { + // There is an unknown item in the tuple. + // It could potentially define its own __instancecheck__ + // so it is no longer possible to replace the op with a const load. + out = NULL; + can_replace_op = false; + continue; + } + PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); + if (cls_o && + // Ensure that item is an exact instance of `type` ensuring that + // there is no __instancecheck__ defined. + sym_matches_type(item, &PyType_Type) && + (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o))) + { + out = Py_True; + break; + } + } + if (out) { + sym_set_const(res, out); + if (can_replace_op) { + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } + } + } + } } op(_GUARD_IS_TRUE_POP, (flag -- )) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 41402200c1683e..c25c12c0f18d85 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2514,6 +2514,37 @@ sym_set_const(res, out); REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); } + else if (inst_type && sym_matches_type(cls, &PyTuple_Type)) { + int length = sym_tuple_length(cls); + if (length != -1) { + bool can_replace_op = true; + PyObject *out = Py_False; + for (int i = 0; i < length; i++) { + JitOptRef item = sym_tuple_getitem(ctx, cls, i); + if (!sym_has_type(item)) { + out = NULL; + can_replace_op = false; + continue; + } + PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); + if (cls_o && + // Ensure that item is an exact instance of `type` ensuring that + // there is no __instancecheck__ defined. + sym_matches_type(item, &PyType_Type) && + (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o))) + { + out = Py_True; + break; + } + } + if (out) { + sym_set_const(res, out); + if (can_replace_op) { + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } + } + } + } stack_pointer[-4] = res; stack_pointer += -3; assert(WITHIN_STACK_BOUNDS());