Skip to content

gh-151907: Avoid creating temporary objects in some comprehensions#151908

Open
ZeroIntensity wants to merge 10 commits into
python:mainfrom
ZeroIntensity:comprehension-avoid-creation
Open

gh-151907: Avoid creating temporary objects in some comprehensions#151908
ZeroIntensity wants to merge 10 commits into
python:mainfrom
ZeroIntensity:comprehension-avoid-creation

Conversation

@ZeroIntensity

@ZeroIntensity ZeroIntensity commented Jun 22, 2026

Copy link
Copy Markdown
Member

The following code:

[i for i in range(42)]

is essentially turned into:

for i in range(42):
    pass

Comment thread Python/codegen.c
VISIT(c, expr, elt);
ADDOP(c, elt_loc, POP_TOP);
break;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we instead just wrap the LIST_APPEND in if !avoid_creation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think we'd also need to wrap the LIST_EXTEND with that too; I opted for this approach to avoid duplicating that logic.

But do we need the separate VISIT path for the Starred case at all? I might be able to refactor it to look like this:

ISIT(c, expr, elt);
if (elt->kind == Starred_kind) {
    op = LIST_EXTEND;
}
else {
    op = LIST_APPEND;
}
if (!avoid_creation) {
    ADDOP_I(c, elt_loc, op, depth + 1);
}

@iritkatriel

Copy link
Copy Markdown
Member

I think this might change semantics for the dict case - if a key is not hashable.

@ZeroIntensity

Copy link
Copy Markdown
Member Author

Yeah, good catch. This breaks set too:

{x for x in [[]]}  # No error!

I'll just remove set and dict for now, but it might still be possible to avoid object creation by calling PyObject_Hash. Not worth doing that here though.

This was a breaking change because hash() was no longer called on the
elements.
@iritkatriel

Copy link
Copy Markdown
Member

Yeah, good catch. This breaks set too:

{x for x in [[]]}  # No error!

I'll just remove set and dict for now, but it might still be possible to avoid object creation by calling PyObject_Hash. Not worth doing that here though.

Maybe also add tests for these cases.

Comment thread Lib/test/test_compiler_codegen.py Outdated
]
self.codegen_test(snippet, expected)

def test_no_target_comp_optimization(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe you need to add a test where the loop body has a side effect. IIUC, the body with thread.join() should be turned into a method call, rather than just pass.

@iritkatriel

Copy link
Copy Markdown
Member

The async case is not handled correctly here. This segfaults:

import asyncio
async def foo(XS):
    [x async for x in XS]

async def agen():
     yield 1
     yield 2
asyncio.run(foo(agen()))
Assertion failed: (PyList_Check(self)), function _PyList_AppendTakeRef, file pycore_list.h, line 40.
zsh: abort      ./python.exe

@ZeroIntensity

Copy link
Copy Markdown
Member Author

Fixed.

Avoids an "env-changed" error with regrtest.
Comment thread Lib/test/test_compiler_codegen.py Outdated
Comment thread Python/codegen.c
if (value->kind == ListComp_kind) {
/* Optimization: Don't bother creating structures if they won't be
* used. */
return codegen_listcomp_impl(c, value, /*avoid_creation=*/true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be nicer if we were not calling codegen_listcomp_impl here but rather setting a boolean on the compiler_unit struct to track whether we are in the topmost expression or not. What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not opposed to the idea, but it feels slightly weird in cases where the thing being compiled isn't an expression. A compiler->has_no_target flag in a FunctionDef, for example, has no real meaning (because compiler->has_no_target == 0 would imply "has target", but that doesn't make sense for a statement). I do think it'll make future optimizations like this easier, though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was thinking of a flag like is_subexpression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

How would I implement that? I can't do like METADATA(c)->u_is_subexpression = 1 in codegen_stmt_expr because that would also affect listcomps inside the one we're optimizing (like [x for x in [y for y in z]]). I could add a stack or something like that, but that would significantly increase the complexity of this PR.

Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
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.

3 participants