-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-126835: Disable tuple folding in the AST optimizer #128802
Conversation
Why have tests been removed from test_compile and test_peepholer? The tests in test_opcache should not be removed, but should be changed to keep the unpacking operation by moving the tuple out of the loop. for ...
a, b = 1, 2 use t = 1, 2
for ...
a, b = t |
Thank you, for some reason I forgot about non-constant case... |
No. You can remove that test. Try to keep any tests that test behavior, but any that just test artifacts (like the test for
Yes, leave that for another PR. As an aside: |
I think we could do that in the follow-up PR. @markshannon do you think it's ok to merge this PR? This PR only removes parts of test_ast which are basically tests AST optimizations, and incorrect test in test_compile. Any other changes are simply commenting some parts of tests, which we can uncomment later when other optimizations will take place in CFG. |
This normally should not be done in the parser as the general idea is that the parser gives the most pure AST to the next step in the pipeline and every modification and optimisations happens there. There are many reasons for this but the classical one is to not make the life of formatters and similar tools hard and not to make unparsing harder. |
I guess you mean to use the word "hard" here. Otherwise, do the parser team make someone's life harder on purpose? 🤣 |
Haha, indeed! 😆
Maybe just our own life 😉 |
FYI, I plan to merge this tomorrow, as this PR is quite small and I don't see any points here which need further discussion. Mark's comments have already been addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this PR will need documentation because ast.parse no longer optimises this, and it's part of the stdlib API. If the optimisations in final bytecode changed then it needs to be mentioned in WhatsNew.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
…en to CFG (#129426) Codegen phase has an optimization that transforms ``` LOAD_CONST x LOAD_CONST y LOAD_CONXT z BUILD_LIST/BUILD_SET (3) ``` -> ``` BUILD_LIST/BUILD_SET (0) LOAD_CONST (x, y, z) LIST_EXTEND/SET_UPDATE 1 ``` This optimization has now been moved to CFG phase to make #128802 work. Co-authored-by: Irit Katriel <[email protected]> Co-authored-by: Yan Yanchii <[email protected]>
… codegen to CFG (python#129426) Codegen phase has an optimization that transforms ``` LOAD_CONST x LOAD_CONST y LOAD_CONXT z BUILD_LIST/BUILD_SET (3) ``` -> ``` BUILD_LIST/BUILD_SET (0) LOAD_CONST (x, y, z) LIST_EXTEND/SET_UPDATE 1 ``` This optimization has now been moved to CFG phase to make python#128802 work. Co-authored-by: Irit Katriel <[email protected]> Co-authored-by: Yan Yanchii <[email protected]>
cc @iritkatriel @WolframAlph for review |
# Long tuples should be folded too. | ||
code = compile(repr(tuple(range(10000))),'','single') | ||
# Long tuples should be folded too, but their length should not | ||
# exceed the `STACK_USE_GUIDELINE` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we perhaps add a test that tuples longer than STACK_USE_GUIDELINE
are in fact not folded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about it. At the moment, for constant tuples which are longer than STACK_USE_GUIDELINE
will be generated following bytecode:
BUILD_LIST 0
Pairs of LOAD_CONST + LIST_APPEND
CALL_INTRINSIC_1 (INTRINSIC_LIST_TO_TUPLE)
Shall we assert that this intrinsic is presented in bytecode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I did a quick review and left some comments.
Lib/test/test_compile.py
Outdated
# Merge constants in tuple or frozenset | ||
f1, f2 = lambda: "not a name", lambda: ("not a name",) | ||
f3 = lambda x: x in {("not a name",)} | ||
self.assertIs(f1.__code__.co_consts[0], | ||
f2.__code__.co_consts[0][0]) | ||
self.assertIs(next(iter(f3.__code__.co_consts[1])), | ||
f2.__code__.co_consts[0]) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these tests removed? They are not related. I understand this is failing, but its enough to update co_consts
index. Now that tuple is folded later in pipeline, it sits at different index. Right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also bring us back to #130016. We wouldn't need to touch this if we were to merge that. @iritkatriel are we planning to?
Lib/test/test_peepholer.py
Outdated
@@ -345,6 +346,28 @@ def negzero(): | |||
self.assertInBytecode(code, opname) | |||
self.check_lnotab(code) | |||
|
|||
def test_folding_of_tuples_on_constants(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_folding_of_tuples_on_constants(self): | |
def test_folding_of_tuples_of_constants(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we actually, already have test_folding_of_tuples_of_constants
, maybe we could add more to those instead?
# Long tuples should be folded too, but their length should not | ||
# exceed the `STACK_USE_GUIDELINE` | ||
code = compile(repr(tuple(range(30))),'','single') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How much of a concern is that we can no longer create constant tuples beyond length of STACK_USE_GUIDELINE
? @iritkatriel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably not ideal in case you have some kind of large constant lookup table for instance. As Kirill pointed out, this would get compiled to a bunch of LOAD_CONST
+ LIST_APPEND
which is probably much slower
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not a hard pattern to detect, right? Maybe we could fold it anyway? @Eclips4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems quite predictable:
>>> def foo():
... return (1,2,3, ... ,31)
...
>>> dis.dis(foo)
1 RESUME 0
2 BUILD_LIST 0
LOAD_SMALL_INT 1
LIST_APPEND 1
LOAD_SMALL_INT 2
LIST_APPEND 1
...
LOAD_SMALL_INT 31
LIST_APPEND 1
CALL_INTRINSIC_1 6 (INTRINSIC_LIST_TO_TUPLE)
(Could also be LOAD_CONST
instead of LOAD_SMALL_INT
here I suppose)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. I think we can easily fold it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could check with is_const_tuple
if the tuple is constant and if it is, ignore STACK_USE_GUIDELINE
because we know it'll be folded by flowgraph anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's a good idea. We would be creating dependency between both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if we are going to fold constant tuple beyond STACK_USE_GUIDELINE
, maybe we could also add this optimization to literal lists & sets? It would be consistent with previous Python version as we are not folding these cases anymore after we migrated them to CFG.
Lib/test/test_builtin.py
Outdated
for opt in [opt1, opt2]: | ||
opt_right = opt.value.right # expect Constant((1,2)) | ||
self.assertIsInstance(opt_right, ast.Constant) | ||
self.assertEqual(opt_right.value, (1, 2)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test tests whether we optimize ast when passed PyCF_ONLY_AST
/PyCF_OPTIMIZED_AST
flag, right? I guess it's not right to just remove lines testing optimized ast. I know there are no more foldings left in ast optimizer, but there is still __debug__
thing left there. So maybe rewrite test to include that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's ok to modify this one, and remove others from test_ast
. I don't think it's worth keeping it. I wrote them to make sure that under different circumstances nodes are still optimized. Since there are no actual foldings (except the __debug__
but it's a special one), I decided to remove them. Changing test_ast
tests to use __debug__
seems overhelming to me, IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you're right. They are testing const folding which no longer is there and __debug__
is just special case (which I am not sure we can call const folding either).
def test_folding_type_param_in_function_def(self): | ||
code = "def foo[%s = (1, 2)](): pass" | ||
|
||
unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) | ||
unoptimized_type_params = [ | ||
("T", "T", ast.TypeVar), | ||
("**P", "P", ast.ParamSpec), | ||
("*Ts", "Ts", ast.TypeVarTuple), | ||
] | ||
|
||
for type, name, type_param in unoptimized_type_params: | ||
result_code = code % type | ||
optimized_target = self.wrap_statement( | ||
ast.FunctionDef( | ||
name='foo', | ||
args=ast.arguments(), | ||
body=[ast.Pass()], | ||
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))] | ||
) | ||
) | ||
non_optimized_target = self.wrap_statement( | ||
ast.FunctionDef( | ||
name='foo', | ||
args=ast.arguments(), | ||
body=[ast.Pass()], | ||
type_params=[type_param(name=name, default_value=unoptimized_tuple)] | ||
) | ||
) | ||
self.assert_ast(result_code, non_optimized_target, optimized_target) | ||
|
||
def test_folding_type_param_in_class_def(self): | ||
code = "class foo[%s = (1, 2)]: pass" | ||
|
||
unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) | ||
unoptimized_type_params = [ | ||
("T", "T", ast.TypeVar), | ||
("**P", "P", ast.ParamSpec), | ||
("*Ts", "Ts", ast.TypeVarTuple), | ||
] | ||
|
||
for type, name, type_param in unoptimized_type_params: | ||
result_code = code % type | ||
optimized_target = self.wrap_statement( | ||
ast.ClassDef( | ||
name='foo', | ||
body=[ast.Pass()], | ||
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))] | ||
) | ||
) | ||
non_optimized_target = self.wrap_statement( | ||
ast.ClassDef( | ||
name='foo', | ||
body=[ast.Pass()], | ||
type_params=[type_param(name=name, default_value=unoptimized_tuple)] | ||
) | ||
) | ||
self.assert_ast(result_code, non_optimized_target, optimized_target) | ||
|
||
def test_folding_type_param_in_type_alias(self): | ||
code = "type foo[%s = (1, 2)] = 1" | ||
|
||
unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) | ||
unoptimized_type_params = [ | ||
("T", "T", ast.TypeVar), | ||
("**P", "P", ast.ParamSpec), | ||
("*Ts", "Ts", ast.TypeVarTuple), | ||
] | ||
|
||
for type, name, type_param in unoptimized_type_params: | ||
result_code = code % type | ||
optimized_target = self.wrap_statement( | ||
ast.TypeAlias( | ||
name=ast.Name(id='foo', ctx=ast.Store()), | ||
type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))], | ||
value=ast.Constant(value=1), | ||
) | ||
) | ||
non_optimized_target = self.wrap_statement( | ||
ast.TypeAlias( | ||
name=ast.Name(id='foo', ctx=ast.Store()), | ||
type_params=[type_param(name=name, default_value=unoptimized_tuple)], | ||
value=ast.Constant(value=1), | ||
) | ||
) | ||
self.assert_ast(result_code, non_optimized_target, optimized_target) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as with test_compile_ast
. Maybe not just remove them, but add testing of __debug__
as it is still in ast optimizer?
def test_optimization_levels_const_folding(self): | ||
folded = ('Expr', (1, 0, 1, 6), ('Constant', (1, 0, 1, 6), (1, 2), None)) | ||
not_folded = ('Expr', (1, 0, 1, 6), | ||
('Tuple', (1, 0, 1, 6), | ||
[('Constant', (1, 1, 1, 2), 1, None), | ||
('Constant', (1, 4, 1, 5), 2, None)], ('Load',))) | ||
|
||
cases = [(-1, not_folded), (0, not_folded), (1, folded), (2, folded)] | ||
for (optval, expected) in cases: | ||
with self.subTest(optval=optval): | ||
tree1 = ast.parse("(1, 2)", optimize=optval) | ||
tree2 = ast.parse(ast.parse("(1, 2)"), optimize=optval) | ||
for tree in [tree1, tree2]: | ||
res = to_tuple(tree.body[0]) | ||
self.assertEqual(res, expected) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as with test_compile_ast
. Maybe add test for __debug__
instead of removing test entirely?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, there is test_optimization_levels__debug__
right above this one, so this one can indeed be gone.
This disables tuple-folding in the AST optimizer which allows the flowgraph to optimize them instead.
Related comment: #126830 (comment)
I've done some local benchmarking with pyperf which showed some speedup for
unpack_sequence
but it'd be nice to have proper benchmarks for this :)cc @Eclips4