gh-131798: JIT: Further optimize _CALL_ISINSTANCE for class tuples#134543
gh-131798: JIT: Further optimize _CALL_ISINSTANCE for class tuples#134543tomasr8 wants to merge 12 commits intopython:mainfrom
_CALL_ISINSTANCE for class tuples#134543Conversation
_CALL_ISINSTANCE for class tuples_CALL_ISINSTANCE for class tuples
| self.assertIn("_BUILD_TUPLE", uops) | ||
| self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) |
There was a problem hiding this comment.
_BUILD_TUPLE is preventing us from optimizing out _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW.
The bytecode is basically:
LOAD_CONST
LOAD_CONST
_BUILD_TUPLE
_POP_CALL_TWO_LOAD_CONST_INLINE_BORROWTo optimize this, we'd need some special handling for _BUILD_TUPLE in remove_unneeded_uops.
There was a problem hiding this comment.
Got it, might be worth looking into next if you're up for it! Could be tricky, though.
|
When you're done making the requested changes, leave the comment: |
So that was a lie 😆 Anyway, I think I addressed all your comments! :) I have made the requested changes; please review again |
|
Thanks for making the requested changes! @brandtbucher: please review the changes made to this pull request. |
brandtbucher
left a comment
There was a problem hiding this comment.
Looks good, just a few suggestions:
| self.assertIn("_BUILD_TUPLE", uops) | ||
| self.assertIn("_POP_CALL_TWO_LOAD_CONST_INLINE_BORROW", uops) |
There was a problem hiding this comment.
Got it, might be worth looking into next if you're up for it! Could be tricky, though.
| 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)) |
There was a problem hiding this comment.
I'm confused, why can't we narrow to True? We can't remove the call, but the result is known.
There was a problem hiding this comment.
Yep, that was a brainfart 😄 I somehow conflated narrowing to True/False with replacing the op, but we can obviously just narrow without removing the call as you said!
| def testfunc(n): | ||
| x = 0 | ||
| for _ in range(n): | ||
| y = isinstance(42, ()) |
There was a problem hiding this comment.
I had to pause and think for a second to figure out what this would even do. Nice edge case.
Python/optimizer_bytecodes.c
Outdated
| // It could potentially define its own __instancecheck__ | ||
| // method so we can only deduce bool. | ||
| out = NULL; | ||
| break; |
There was a problem hiding this comment.
Could this be continue? We could still narrow to True later to handle cases like the one I called out above, right?
| break; | |
| continue; |
There was a problem hiding this comment.
Yup, see #134543 (comment). I just needed to add some extra bookkeeping to know when we can replace the call and when not.
| } | ||
| PyTypeObject *cls_o = (PyTypeObject *)sym_get_const(ctx, item); | ||
| if (cls_o && | ||
| sym_matches_type(item, &PyType_Type) && |
There was a problem hiding this comment.
Maybe add a comment explaining that this is to protect against metaclasses definine __instancecheck__.
There was a problem hiding this comment.
How would you formulate it? I don't think of it as specifically a guard for __instancecheck__ but basically PyObject_TypeCheck adapted to the JIT optimizer.
There was a problem hiding this comment.
We're not only checking that the object is a subclass of type, we're also checking that it is an exact instance of type itself. We care about this second condition because it guarantees that __instancecheck__ doesn't exist (otherwise we would need to look it up to check if it exists or not).
There was a problem hiding this comment.
Thanks for the clarification, I added a comment :)
|
Off-topic, but something I've been thinking about that you may want to check out next: removing bounds checks when indexing into strings and tuples with a constant index. |
|
Thanks for the review! Super helpful as always :) I'll leave this open for a few days so @Fidget-Spinner can also have a look :)
Sure, I can look into that! I guess this will also require knowing the string/tuple length? |
| 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) && |
There was a problem hiding this comment.
Wait, wouldn't this only work on something that is isinstance(x, type), like the actual type itself, not any type like int? Sorry, I'm confused!
There was a problem hiding this comment.
Hmm I don't believe so. If item is e.g. int then sym_matches_type(item, &PyType_Type) should return true
There was a problem hiding this comment.
For example something like this seems to work:
JitOptRef ref_type = _Py_uop_sym_new_const(ctx, (PyObject *)&PyLong_Type);
TEST_PREDICATE(_Py_uop_sym_matches_type(ref_type, &PyType_Type), "int is not a type");There was a problem hiding this comment.
That''s kinda strange. The code for _Py_uop_sym_matches_type is:
_Py_uop_sym_get_type(sym) == typ
so it's a pointer comparison, not a subclass check.
There was a problem hiding this comment.
The subclass check is done after with PyType_IsSubtype if that's what you mean? The current version of _CALL_ISINSTANCE (which does not handle tuples) works the same way though. @brandtbucher, what do you think?
|
I'm really sorry Tomas, but the more I look at the code, the more I feel the complexity is not worth it in this case. |
Fidget-Spinner
left a comment
There was a problem hiding this comment.
We need a test for a type with __instancecheck__, otherwise it LGTM, and you can merge after the test is added
We can already const eval
isinstance(inst, cls)calls when both arguments are known, but only ifclsis a single class (e.g.int).This PR adds support for
isinstance(inst, (cls1, cls2, ..., clsN)). This allows us to optimize for example:isinstance(42, (int, str))(toTrue)isinstance(42, (bool, str))(toFalse)We can narrow to
Trueeven when only some of the classes are known, as long asinstis an instance of one of the known classes.