Skip to content
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

Closed
wants to merge 23 commits into from

Conversation

tomasr8
Copy link
Member

@tomasr8 tomasr8 commented Jan 13, 2025

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

@markshannon
Copy link
Member

Why have tests been removed from test_compile and test_peepholer?
Those test the code after the CFG optimizer has run, so should be unchanged.

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.
Instead of:

for ...
    a, b = 1, 2

use

t = 1, 2
for ...
    a, b = t

@Eclips4
Copy link
Member

Eclips4 commented Jan 21, 2025

Why have tests been removed from test_compile and test_peepholer? Those test the code after the CFG optimizer has run, so should be unchanged.

test_compile was changed because the test was fundamentally wrong. The name of the test suggests that it tests constant merging, but actually, all the work was done by AST optimizer. Constant merging has never had any relation to this test. In particular, it tested that co_consts for lambda: (257,) would look like a ((257,),) instead of (257, (257,)). Do you think it's worth fixing?

test_peepholer only changed where it tries to fold set into frozenset where one of the elements is constant tuple. Since folding of constant tuples are handled by the CFG, set folding during the AST optimization can't work for such case. We could add folding for sets (not in all cases, only with in operator and where set is a target for for loop) in the follow-up PR.

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.

Thank you, for some reason I forgot about non-constant case...

@markshannon
Copy link
Member

Constant merging has never had any relation to this test. In particular, it tested that co_consts for lambda: (257,) would look like a ((257,),) instead of (257, (257,)). Do you think it's worth fixing?

No. You can remove that test. Try to keep any tests that test behavior, but any that just test artifacts (like the test for lambda: (257,) ) can be removed.

We could add folding for sets (not in all cases, only with in operator and where set is a target for for loop) in the follow-up PR.

Yes, leave that for another PR.

As an aside:
From a quick experiment I did a while ago, I think the only folding that needs to be done before the CFG optimizer is for negative numbers, converting -(1) into -1. I think that can be done in the codegen pass.

@Eclips4 Eclips4 marked this pull request as ready for review January 21, 2025 14:11
@Eclips4
Copy link
Member

Eclips4 commented Jan 21, 2025

As an aside: From a quick experiment I did a while ago, I think the only folding that needs to be done before the CFG optimizer is for negative numbers, converting -(1) into -1. I think that can be done in the codegen pass.

I think we could do that in the follow-up PR.
Though, I'm not an parser expert, but when I understood that parser generates ast.UnaryOp node instead of ast.Constant for simple constants like -1, I was confused.
@pablogsal as parser expert, is it hard to do this in the parser?

@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.

@pablogsal
Copy link
Member

pablogsal commented Jan 21, 2025

@pablogsal as parser expert, is it hard to do this in the parser?

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.

@Eclips4
Copy link
Member

Eclips4 commented Jan 21, 2025

There are many reasons for this but the classical one is to not make the life of formatters and similar tools easy

I guess you mean to use the word "hard" here. Otherwise, do the parser team make someone's life harder on purpose? 🤣

@pablogsal
Copy link
Member

I guess you mean to use the word "hard" here.

Haha, indeed! 😆

Otherwise, do the parser team make someone's life harder on purpose? 🤣

Maybe just our own life 😉

@Eclips4
Copy link
Member

Eclips4 commented Jan 26, 2025

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.

Copy link
Member

@iritkatriel iritkatriel left a 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.

@bedevere-app
Copy link

bedevere-app bot commented Jan 27, 2025

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Eclips4 added a commit that referenced this pull request Feb 1, 2025
…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]>
srinivasreddy pushed a commit to srinivasreddy/cpython that referenced this pull request Feb 7, 2025
… 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]>
@Eclips4 Eclips4 marked this pull request as ready for review February 22, 2025 10:38
@Eclips4 Eclips4 marked this pull request as draft February 22, 2025 10:38
@Eclips4 Eclips4 marked this pull request as ready for review February 22, 2025 13:29
@Eclips4
Copy link
Member

Eclips4 commented Feb 22, 2025

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`
Copy link
Member Author

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?

Copy link
Member

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?

Copy link
Contributor

@WolframAlph WolframAlph left a 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.

Comment on lines 796 to 803
# 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])

Copy link
Contributor

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?

Copy link
Contributor

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?

@@ -345,6 +346,28 @@ def negzero():
self.assertInBytecode(code, opname)
self.check_lnotab(code)

def test_folding_of_tuples_on_constants(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_folding_of_tuples_on_constants(self):
def test_folding_of_tuples_of_constants(self):

Copy link
Contributor

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?

Comment on lines +167 to +169
# Long tuples should be folded too, but their length should not
# exceed the `STACK_USE_GUIDELINE`
code = compile(repr(tuple(range(30))),'','single')
Copy link
Contributor

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

Copy link
Member Author

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

Copy link
Contributor

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

Copy link
Member Author

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)

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Contributor

@WolframAlph WolframAlph Feb 24, 2025

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.

Comment on lines 573 to 577
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))

Copy link
Contributor

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?

Copy link
Member

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

Copy link
Contributor

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).

Comment on lines -3150 to -3234
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)
Copy link
Contributor

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?

Comment on lines -156 to -171
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)

Copy link
Contributor

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?

Copy link
Contributor

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.

@WolframAlph
Copy link
Contributor

@tomasr8 I believe this one can be closed in favour of #130769

@Eclips4 Eclips4 closed this Mar 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants