From 1d582e8d6979c70c172c048a6184eebd2fd59654 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 22 May 2025 22:20:20 +0200 Subject: [PATCH 1/9] Add news entry --- .../2025-05-22-22-19-56.gh-issue-131798.kxRt1-.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-22-22-19-56.gh-issue-131798.kxRt1-.rst 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. From 97aca361d55c5de4a171886090aa51665a05b196 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 22 May 2025 22:20:42 +0200 Subject: [PATCH 2/9] Optimize _CALL_ISINSTANCE for class tuples --- Lib/test/test_capi/test_opt.py | 86 ++++++++++++++++++++++++++++++++-- Python/optimizer_bytecodes.c | 48 ++++++++++++++++++- Python/optimizer_cases.c.h | 34 +++++++++++++- 3 files changed, 162 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index cb6eae484149ee..e9ea777347797d 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2105,17 +2105,76 @@ 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(self): + def testfunc(n): + x = 0 + for _ in range(n): + # One of the classes is unknown, but we can still + # narrow to True + 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.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_unknown_not_narrowed(self): + def testfunc(n): + x = 0 + for _ in range(n): + # One of the classes is unknown, so we can't narrow + # to True or False, only bool + 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) @@ -2124,6 +2183,27 @@ def testfunc(n): 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_metaclass(self): class EvenNumberMeta(type): def __instancecheck__(self, number): diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 49c6bfb6c1b01a..a933a1d7fd4adb 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -938,6 +938,9 @@ dummy_func(void) { } op(_CALL_ISINSTANCE, (unused, unused, instance, cls -- res)) { +// The below define is equivalent to PyObject_TypeCheck(inst, cls) +#define sym_IS_SUBTYPE(inst, cls) ((inst) == (cls) || PyType_IsSubtype(inst, cls)) + // the result is always a bool, but sometimes we can // narrow it down to True or False res = sym_new_type(ctx, &PyBool_Type); @@ -947,14 +950,55 @@ 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)) { + if (sym_IS_SUBTYPE(inst_type, cls_o)) { out = Py_True; } 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); + bool all_items_known = true; + PyObject *out = NULL; + if (length >= 0) { + // We cannot do anything about tuples with unknown (length == -1) + + for (int i = 0; i < length; i++) { + JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); + if (!sym_has_type(item)) { + // There is an unknown item in the tuple, + // we can no longer deduce False. + all_items_known = false; + continue; + } + PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); + if (cls_o && + sym_matches_type(item, &PyType_Type) && + sym_IS_SUBTYPE(inst_type, cls_o)) + { + out = Py_True; + break; + } + } + if (!out && all_items_known) { + // We haven't deduced True, but all items in the tuple are known + // so we can deduce False + out = Py_False; + } + if (out) { + sym_set_const(res, out); + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } + } + } +#undef sym_IS_SUBTYPE } op(_GUARD_IS_TRUE_POP, (flag -- )) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index bf7ac72d4579e7..8589cad56f1f34 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2201,17 +2201,49 @@ JitOptSymbol *res; cls = stack_pointer[-1]; instance = stack_pointer[-2]; + #define sym_IS_SUBTYPE(inst, cls) ((inst) == (cls) || PyType_IsSubtype(inst, cls)) + res = sym_new_type(ctx, &PyBool_Type); PyTypeObject *inst_type = sym_get_type(instance); PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, cls); if (inst_type && cls_o && sym_matches_type(cls, &PyType_Type)) { PyObject *out = Py_False; - if (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o)) { + if (sym_IS_SUBTYPE(inst_type, cls_o)) { out = Py_True; } 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); + bool all_items_known = true; + PyObject *out = NULL; + if (length >= 0) { + for (int i = 0; i < length; i++) { + JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); + if (!sym_has_type(item)) { + all_items_known = false; + continue; + } + PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); + if (cls_o && + sym_matches_type(item, &PyType_Type) && + sym_IS_SUBTYPE(inst_type, cls_o)) + { + out = Py_True; + break; + } + } + if (!out && all_items_known) { + out = Py_False; + } + if (out) { + sym_set_const(res, out); + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } + } + } + #undef sym_is_subtype stack_pointer[-4] = res; stack_pointer += -3; assert(WITHIN_STACK_BOUNDS()); From 467bcb9eac17de46f7e8cc6252ee9e37cab74984 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Thu, 22 May 2025 22:52:49 +0200 Subject: [PATCH 3/9] Add more tests --- Lib/test/test_capi/test_opt.py | 19 +++++++++++++++++++ Python/optimizer_bytecodes.c | 9 ++++----- Python/optimizer_cases.c.h | 8 ++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index e9ea777347797d..ad91e32079ddb1 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2204,6 +2204,25 @@ def testfunc(n): 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) + 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_metaclass(self): class EvenNumberMeta(type): def __instancecheck__(self, number): diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index a933a1d7fd4adb..7907f931e34915 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -965,11 +965,10 @@ dummy_func(void) { // inst is not an instance of any of them. int length = sym_tuple_length(cls); - bool all_items_known = true; - PyObject *out = NULL; - if (length >= 0) { - // We cannot do anything about tuples with unknown (length == -1) - + if (length != -1) { + // We cannot do anything about tuples with unknown length + bool all_items_known = true; + PyObject *out = NULL; for (int i = 0; i < length; i++) { JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 8589cad56f1f34..6c9cd9049b3c2e 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2216,9 +2216,9 @@ } else if (inst_type && sym_matches_type(cls, &PyTuple_Type)) { int length = sym_tuple_length(cls); - bool all_items_known = true; - PyObject *out = NULL; - if (length >= 0) { + if (length != -1) { + bool all_items_known = true; + PyObject *out = NULL; for (int i = 0; i < length; i++) { JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { @@ -2243,7 +2243,7 @@ } } } - #undef sym_is_subtype + #undef sym_IS_SUBTYPE stack_pointer[-4] = res; stack_pointer += -3; assert(WITHIN_STACK_BOUNDS()); From d2e339f98775753b0abb78ca1f52d68be9d9ce7b Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Jul 2025 23:17:21 +0200 Subject: [PATCH 4/9] Bail when encountering an unknown class --- Lib/test/test_capi/test_opt.py | 80 +++++++++++++++++++++++++++++++--- Python/optimizer_bytecodes.c | 19 ++++---- Python/optimizer_cases.c.h | 11 ++--- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index ad91e32079ddb1..263c4a0133889c 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2143,13 +2143,13 @@ def testfunc(n): self.assertIn("_BUILD_TUPLE", uops) self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) - def test_call_isinstance_tuple_of_classes_true_unknown(self): + 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 we can still - # narrow to True - y = isinstance(42, (eval('str'), int)) + # One of the classes is unknown, but it comes + # after a known class, so we can narrow to True + y = isinstance(42, (int, eval('str'))) if y: x += 1 return x @@ -2160,11 +2160,30 @@ def testfunc(n): uops = get_opnames(ex) self.assertNotIn("_CALL_ISINSTANCE", uops) self.assertNotIn("_TO_BOOL_BOOL", uops) - self.assertNotIn("_GUARD_IS_TRUE_POP", 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_unknown_not_narrowed(self): + def test_call_isinstance_tuple_of_classes_true_unknown_2(self): + def testfunc(n): + x = 0 + for _ in range(n): + # One of the classes is unknown, so we can't narrow + # to True or False, only bool + 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.assertIn("_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): @@ -2183,6 +2202,25 @@ def testfunc(n): 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): + # One of the classes is unknown, so we can't narrow + # to True or False, only bool + 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 @@ -2248,6 +2286,36 @@ def testfunc(n): self.assertNotIn("_TO_BOOL_BOOL", uops) self.assertIn("_GUARD_IS_TRUE_POP", uops) + def test_call_isinstance_tuple_metaclass(self): + calls = 0 + + class Meta(type): + def __instancecheck__(self, _): + nonlocal calls + calls += 1 + return False + + class Unknown(metaclass=Meta): + pass + + def testfunc(n): + x = 0 + for _ in range(n): + # Only narrowed to bool + y = isinstance(42, (Unknown, int)) + if y: + x += 1 + return x, calls + + (res, calls), ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) + self.assertEqual(res, TIER2_THRESHOLD) + self.assertEqual(calls, 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_set_type_version_sets_type(self): class C: A = 1 diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 7907f931e34915..c854370810d266 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -938,9 +938,6 @@ dummy_func(void) { } op(_CALL_ISINSTANCE, (unused, unused, instance, cls -- res)) { -// The below define is equivalent to PyObject_TypeCheck(inst, cls) -#define sym_IS_SUBTYPE(inst, cls) ((inst) == (cls) || PyType_IsSubtype(inst, cls)) - // the result is always a bool, but sometimes we can // narrow it down to True or False res = sym_new_type(ctx, &PyBool_Type); @@ -951,7 +948,7 @@ dummy_func(void) { // known types, meaning we can deduce either True or False PyObject *out = Py_False; - if (sym_IS_SUBTYPE(inst_type, cls_o)) { + if (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o)) { out = Py_True; } sym_set_const(res, out); @@ -972,23 +969,24 @@ dummy_func(void) { for (int i = 0; i < length; i++) { JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { - // There is an unknown item in the tuple, - // we can no longer deduce False. + // There is an unknown item in the tuple. + // It could potentially define its own __instancecheck__ + // method so we can only deduce bool. all_items_known = false; - continue; + break; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); if (cls_o && sym_matches_type(item, &PyType_Type) && - sym_IS_SUBTYPE(inst_type, cls_o)) + (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o))) { out = Py_True; break; } } - if (!out && all_items_known) { + if (out == NULL && all_items_known) { // We haven't deduced True, but all items in the tuple are known - // so we can deduce False + // so we can deduce False. out = Py_False; } if (out) { @@ -997,7 +995,6 @@ dummy_func(void) { } } } -#undef sym_IS_SUBTYPE } op(_GUARD_IS_TRUE_POP, (flag -- )) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 6c9cd9049b3c2e..1edc309658c28d 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2201,14 +2201,12 @@ JitOptSymbol *res; cls = stack_pointer[-1]; instance = stack_pointer[-2]; - #define sym_IS_SUBTYPE(inst, cls) ((inst) == (cls) || PyType_IsSubtype(inst, cls)) - res = sym_new_type(ctx, &PyBool_Type); PyTypeObject *inst_type = sym_get_type(instance); PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, cls); if (inst_type && cls_o && sym_matches_type(cls, &PyType_Type)) { PyObject *out = Py_False; - if (sym_IS_SUBTYPE(inst_type, cls_o)) { + if (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o)) { out = Py_True; } sym_set_const(res, out); @@ -2223,18 +2221,18 @@ JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { all_items_known = false; - continue; + break; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); if (cls_o && sym_matches_type(item, &PyType_Type) && - sym_IS_SUBTYPE(inst_type, cls_o)) + (inst_type == cls_o || PyType_IsSubtype(inst_type, cls_o))) { out = Py_True; break; } } - if (!out && all_items_known) { + if (out == NULL && all_items_known) { out = Py_False; } if (out) { @@ -2243,7 +2241,6 @@ } } } - #undef sym_IS_SUBTYPE stack_pointer[-4] = res; stack_pointer += -3; assert(WITHIN_STACK_BOUNDS()); From 0465c9ab43597e101b26b4925b4e37078fd5d96d Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Jul 2025 23:18:56 +0200 Subject: [PATCH 5/9] Remove an extra test case --- Lib/test/test_capi/test_opt.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index 263c4a0133889c..33cac19d4e0815 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2286,36 +2286,6 @@ def testfunc(n): self.assertNotIn("_TO_BOOL_BOOL", uops) self.assertIn("_GUARD_IS_TRUE_POP", uops) - def test_call_isinstance_tuple_metaclass(self): - calls = 0 - - class Meta(type): - def __instancecheck__(self, _): - nonlocal calls - calls += 1 - return False - - class Unknown(metaclass=Meta): - pass - - def testfunc(n): - x = 0 - for _ in range(n): - # Only narrowed to bool - y = isinstance(42, (Unknown, int)) - if y: - x += 1 - return x, calls - - (res, calls), ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD) - self.assertEqual(res, TIER2_THRESHOLD) - self.assertEqual(calls, 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_set_type_version_sets_type(self): class C: A = 1 From 088ccd84a12216942d0295b6cbe8c4f61b4bbd98 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Jul 2025 22:58:33 +0200 Subject: [PATCH 6/9] Simplify code --- Python/optimizer_bytecodes.c | 10 ++-------- Python/optimizer_cases.c.h | 8 ++------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index c854370810d266..33c9752b3dcfa6 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -964,15 +964,14 @@ dummy_func(void) { int length = sym_tuple_length(cls); if (length != -1) { // We cannot do anything about tuples with unknown length - bool all_items_known = true; - PyObject *out = NULL; + PyObject *out = Py_False; for (int i = 0; i < length; i++) { JitOptSymbol *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__ // method so we can only deduce bool. - all_items_known = false; + out = NULL; break; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); @@ -984,11 +983,6 @@ dummy_func(void) { break; } } - if (out == NULL && all_items_known) { - // We haven't deduced True, but all items in the tuple are known - // so we can deduce False. - out = Py_False; - } if (out) { sym_set_const(res, out); REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 1edc309658c28d..48cd0124c82f92 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2215,12 +2215,11 @@ else if (inst_type && sym_matches_type(cls, &PyTuple_Type)) { int length = sym_tuple_length(cls); if (length != -1) { - bool all_items_known = true; - PyObject *out = NULL; + PyObject *out = Py_False; for (int i = 0; i < length; i++) { JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { - all_items_known = false; + out = NULL; break; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); @@ -2232,9 +2231,6 @@ break; } } - if (out == NULL && all_items_known) { - out = Py_False; - } if (out) { sym_set_const(res, out); REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); From 1e9b95fc799b3977c1c1cec51892ba36bcd43a40 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 19 Jul 2025 10:33:54 +0200 Subject: [PATCH 7/9] Fix incorrect type --- Python/optimizer_bytecodes.c | 2 +- Python/optimizer_cases.c.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index 5387325cd06572..bc075a1d232ca8 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -975,7 +975,7 @@ dummy_func(void) { // We cannot do anything about tuples with unknown length PyObject *out = Py_False; for (int i = 0; i < length; i++) { - JitOptSymbol *item = sym_tuple_getitem(ctx, cls, 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__ diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 9ec82cc074d4eb..de4112362ea436 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2519,7 +2519,7 @@ if (length != -1) { PyObject *out = Py_False; for (int i = 0; i < length; i++) { - JitOptSymbol *item = sym_tuple_getitem(ctx, cls, i); + JitOptRef item = sym_tuple_getitem(ctx, cls, i); if (!sym_has_type(item)) { out = NULL; break; From a09b86049aa79ccb0386385a7c2ddb235c9f31df Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 29 Jul 2025 20:47:17 +0200 Subject: [PATCH 8/9] Narrow to true but keep the isinstance call --- Lib/test/test_capi/test_opt.py | 18 +++++++++--------- Python/optimizer_bytecodes.c | 10 +++++++--- Python/optimizer_cases.c.h | 8 ++++++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py index b77a1b81bea0b4..0409a0709048c8 100644 --- a/Lib/test/test_capi/test_opt.py +++ b/Lib/test/test_capi/test_opt.py @@ -2234,7 +2234,8 @@ 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 + # 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 @@ -2246,7 +2247,7 @@ def testfunc(n): uops = get_opnames(ex) self.assertNotIn("_CALL_ISINSTANCE", uops) self.assertNotIn("_TO_BOOL_BOOL", uops) - self.assertNotIn("_GUARD_IS_FALSE_POP", uops) + self.assertNotIn("_GUARD_IS_TRUE_POP", uops) self.assertIn("_BUILD_TUPLE", uops) self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) @@ -2254,8 +2255,9 @@ def test_call_isinstance_tuple_of_classes_true_unknown_2(self): def testfunc(n): x = 0 for _ in range(n): - # One of the classes is unknown, so we can't narrow - # to True or False, only bool + # 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 @@ -2267,14 +2269,13 @@ def testfunc(n): uops = get_opnames(ex) self.assertIn("_CALL_ISINSTANCE", uops) self.assertNotIn("_TO_BOOL_BOOL", uops) - self.assertIn("_GUARD_IS_TRUE_POP", 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): - # One of the classes is unknown, so we can't narrow - # to True or False, only bool + # We can only narrow to bool here y = isinstance(42, (str, eval('int'))) if y: x += 1 @@ -2292,8 +2293,7 @@ def test_call_isinstance_tuple_of_classes_true_unknown_4(self): def testfunc(n): x = 0 for _ in range(n): - # One of the classes is unknown, so we can't narrow - # to True or False, only bool + # We can only narrow to bool here y = isinstance(42, (eval('int'), str)) if y: x += 1 diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index bc075a1d232ca8..cc4a816c9843fb 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -973,15 +973,17 @@ dummy_func(void) { 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__ - // method so we can only deduce bool. + // so it is no longer possible to replace the op with a const load. out = NULL; - break; + can_replace_op = false; + continue; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); if (cls_o && @@ -994,7 +996,9 @@ dummy_func(void) { } if (out) { sym_set_const(res, out); - REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + if (can_replace_op) { + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } } } } diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index de4112362ea436..48108b8471c33e 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2517,12 +2517,14 @@ 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; - break; + can_replace_op = false; + continue; } PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); if (cls_o && @@ -2535,7 +2537,9 @@ } if (out) { sym_set_const(res, out); - REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + if (can_replace_op) { + REPLACE_OP(this_instr, _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)out); + } } } } From 17701d33d1972e6b49467482055e5e19030b1270 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 2 Aug 2025 21:30:09 +0200 Subject: [PATCH 9/9] Add a comment about __instancecheck__ --- Python/optimizer_bytecodes.c | 2 ++ Python/optimizer_cases.c.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c index cc4a816c9843fb..5f80ded47b1de0 100644 --- a/Python/optimizer_bytecodes.c +++ b/Python/optimizer_bytecodes.c @@ -987,6 +987,8 @@ dummy_func(void) { } 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))) { diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h index 48108b8471c33e..c25c12c0f18d85 100644 --- a/Python/optimizer_cases.c.h +++ b/Python/optimizer_cases.c.h @@ -2528,6 +2528,8 @@ } 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))) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy