Skip to content

bpo-32856: Optimize the assignment idiom in comprehensions. #16814

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ signals to a process using a file descriptor instead of a pid. (:issue:`38712`)
Optimizations
=============

* Optimized the idiom for assignment a temporary variable in comprehensions.
Now ``for y in [expr]`` in comprehensions is as fast as a simple assignment
``y = expr``. For example:

sums = [s for s in [0] for x in data for s in [s + x]]

Unlike to the ``:=`` operator this idiom does not leak a variable to the
outer scope.

(Contributed by Serhiy Storchaka in :issue:`32856`.)


Build and C API Changes
=======================
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_dictcomps.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,22 @@ def add_call(pos, value):
self.assertEqual(actual, expected)
self.assertEqual(actual_calls, expected_calls)

def test_assignment_idiom_in_comprehensions(self):
expected = {1: 1, 2: 4, 3: 9, 4: 16}
actual = {j: j*j for i in range(4) for j in [i+1]}
self.assertEqual(actual, expected)
expected = {3: 2, 5: 6, 7: 12, 9: 20}
actual = {j+k: j*k for i in range(4) for j in [i+1] for k in [j+1]}
self.assertEqual(actual, expected)
expected = {3: 2, 5: 6, 7: 12, 9: 20}
actual = {j+k: j*k for i in range(4) for j, k in [(i+1, i+2)]}
self.assertEqual(actual, expected)

def test_star_expression(self):
expected = {0: 0, 1: 1, 2: 4, 3: 9}
self.assertEqual({i: i*i for i in [*range(4)]}, expected)
self.assertEqual({i: i*i for i in (*range(4),)}, expected)


if __name__ == "__main__":
unittest.main()
16 changes: 16 additions & 0 deletions Lib/test/test_genexps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
>>> list((i,j) for i in range(4) for j in range(i) )
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]

Test the idiom for temporary variable assignment in comprehensions.

>>> list((j*j for i in range(4) for j in [i+1]))
[1, 4, 9, 16]
>>> list((j*k for i in range(4) for j in [i+1] for k in [j+1]))
[2, 6, 12, 20]
>>> list((j*k for i in range(4) for j, k in [(i+1, i+2)]))
[2, 6, 12, 20]

Not assignment

>>> list((i*i for i in [*range(4)]))
[0, 1, 4, 9]
>>> list((i*i for i in (*range(4),)))
[0, 1, 4, 9]

Make sure the induction variable is not exposed

>>> i = 20
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_listcomps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@
>>> [(i,j) for i in range(4) for j in range(i)]
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]

Test the idiom for temporary variable assignment in comprehensions.

>>> [j*j for i in range(4) for j in [i+1]]
[1, 4, 9, 16]
>>> [j*k for i in range(4) for j in [i+1] for k in [j+1]]
[2, 6, 12, 20]
>>> [j*k for i in range(4) for j, k in [(i+1, i+2)]]
[2, 6, 12, 20]

Not assignment

>>> [i*i for i in [*range(4)]]
[0, 1, 4, 9]
>>> [i*i for i in (*range(4),)]
[0, 1, 4, 9]

Make sure the induction variable is not exposed

>>> i = 20
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,20 @@ def f(x):
return 6
self.check_lnotab(f)

def test_assignment_idiom_in_comprehensions(self):
def listcomp():
return [y for x in a for y in [f(x)]]
self.assertEqual(count_instr_recursively(listcomp, 'FOR_ITER'), 1)
def setcomp():
return {y for x in a for y in [f(x)]}
self.assertEqual(count_instr_recursively(setcomp, 'FOR_ITER'), 1)
def dictcomp():
return {y: y for x in a for y in [f(x)]}
self.assertEqual(count_instr_recursively(dictcomp, 'FOR_ITER'), 1)
def genexpr():
return (y for x in a for y in [f(x)])
self.assertEqual(count_instr_recursively(genexpr, 'FOR_ITER'), 1)


class TestBuglets(unittest.TestCase):

Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_setcomps.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@
>>> list(sorted({(i,j) for i in range(4) for j in range(i)}))
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]

Test the idiom for temporary variable assignment in comprehensions.

>>> sorted({j*j for i in range(4) for j in [i+1]})
[1, 4, 9, 16]
>>> sorted({j*k for i in range(4) for j in [i+1] for k in [j+1]})
[2, 6, 12, 20]
>>> sorted({j*k for i in range(4) for j, k in [(i+1, i+2)]})
[2, 6, 12, 20]

Not assignment

>>> sorted({i*i for i in [*range(4)]})
[0, 1, 4, 9]
>>> sorted({i*i for i in (*range(4),)})
[0, 1, 4, 9]

Make sure the induction variable is not exposed

>>> i = 20
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Optimized the idiom for assignment a temporary variable in comprehensions.
Now ``for y in [expr]`` in comprehensions is as fast as a simple assignment
``y = expr``.
70 changes: 52 additions & 18 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,13 @@ static int compiler_set_qualname(struct compiler *);
static int compiler_sync_comprehension_generator(
struct compiler *c,
asdl_seq *generators, int gen_index,
int depth,
expr_ty elt, expr_ty val, int type);

static int compiler_async_comprehension_generator(
struct compiler *c,
asdl_seq *generators, int gen_index,
int depth,
expr_ty elt, expr_ty val, int type);

static PyCodeObject *assemble(struct compiler *, int addNone);
Expand Down Expand Up @@ -4289,22 +4291,24 @@ compiler_call_helper(struct compiler *c,
static int
compiler_comprehension_generator(struct compiler *c,
asdl_seq *generators, int gen_index,
int depth,
expr_ty elt, expr_ty val, int type)
{
comprehension_ty gen;
gen = (comprehension_ty)asdl_seq_GET(generators, gen_index);
if (gen->is_async) {
return compiler_async_comprehension_generator(
c, generators, gen_index, elt, val, type);
c, generators, gen_index, depth, elt, val, type);
} else {
return compiler_sync_comprehension_generator(
c, generators, gen_index, elt, val, type);
c, generators, gen_index, depth, elt, val, type);
}
}

static int
compiler_sync_comprehension_generator(struct compiler *c,
asdl_seq *generators, int gen_index,
int depth,
expr_ty elt, expr_ty val, int type)
{
/* generate code for the iterator, then each of the ifs,
Expand Down Expand Up @@ -4332,12 +4336,38 @@ compiler_sync_comprehension_generator(struct compiler *c,
}
else {
/* Sub-iter - calculate on the fly */
VISIT(c, expr, gen->iter);
ADDOP(c, GET_ITER);
/* Fast path for the temporary variable assignment idiom:
for y in [f(x)]
*/
asdl_seq *elts;
switch (gen->iter->kind) {
case List_kind:
elts = gen->iter->v.List.elts;
break;
case Tuple_kind:
elts = gen->iter->v.Tuple.elts;
break;
default:
elts = NULL;
}
if (asdl_seq_LEN(elts) == 1) {
expr_ty elt = asdl_seq_GET(elts, 0);
if (elt->kind != Starred_kind) {
VISIT(c, expr, elt);
start = NULL;
}
}
if (start) {
VISIT(c, expr, gen->iter);
ADDOP(c, GET_ITER);
}
}
if (start) {
depth++;
compiler_use_next_block(c, start);
ADDOP_JREL(c, FOR_ITER, anchor);
NEXT_BLOCK(c);
}
compiler_use_next_block(c, start);
ADDOP_JREL(c, FOR_ITER, anchor);
NEXT_BLOCK(c);
VISIT(c, expr, gen->target);

/* XXX this needs to be cleaned up...a lot! */
Expand All @@ -4351,7 +4381,7 @@ compiler_sync_comprehension_generator(struct compiler *c,

if (++gen_index < asdl_seq_LEN(generators))
if (!compiler_comprehension_generator(c,
generators, gen_index,
generators, gen_index, depth,
elt, val, type))
return 0;

Expand All @@ -4366,18 +4396,18 @@ compiler_sync_comprehension_generator(struct compiler *c,
break;
case COMP_LISTCOMP:
VISIT(c, expr, elt);
ADDOP_I(c, LIST_APPEND, gen_index + 1);
ADDOP_I(c, LIST_APPEND, depth + 1);
break;
case COMP_SETCOMP:
VISIT(c, expr, elt);
ADDOP_I(c, SET_ADD, gen_index + 1);
ADDOP_I(c, SET_ADD, depth + 1);
break;
case COMP_DICTCOMP:
/* With '{k: v}', k is evaluated before v, so we do
the same. */
VISIT(c, expr, elt);
VISIT(c, expr, val);
ADDOP_I(c, MAP_ADD, gen_index + 1);
ADDOP_I(c, MAP_ADD, depth + 1);
break;
default:
return 0;
Expand All @@ -4386,15 +4416,18 @@ compiler_sync_comprehension_generator(struct compiler *c,
compiler_use_next_block(c, skip);
}
compiler_use_next_block(c, if_cleanup);
ADDOP_JABS(c, JUMP_ABSOLUTE, start);
compiler_use_next_block(c, anchor);
if (start) {
ADDOP_JABS(c, JUMP_ABSOLUTE, start);
compiler_use_next_block(c, anchor);
}

return 1;
}

static int
compiler_async_comprehension_generator(struct compiler *c,
asdl_seq *generators, int gen_index,
int depth,
expr_ty elt, expr_ty val, int type)
{
comprehension_ty gen;
Expand Down Expand Up @@ -4438,9 +4471,10 @@ compiler_async_comprehension_generator(struct compiler *c,
NEXT_BLOCK(c);
}

depth++;
if (++gen_index < asdl_seq_LEN(generators))
if (!compiler_comprehension_generator(c,
generators, gen_index,
generators, gen_index, depth,
elt, val, type))
return 0;

Expand All @@ -4455,18 +4489,18 @@ compiler_async_comprehension_generator(struct compiler *c,
break;
case COMP_LISTCOMP:
VISIT(c, expr, elt);
ADDOP_I(c, LIST_APPEND, gen_index + 1);
ADDOP_I(c, LIST_APPEND, depth + 1);
break;
case COMP_SETCOMP:
VISIT(c, expr, elt);
ADDOP_I(c, SET_ADD, gen_index + 1);
ADDOP_I(c, SET_ADD, depth + 1);
break;
case COMP_DICTCOMP:
/* With '{k: v}', k is evaluated before v, so we do
the same. */
VISIT(c, expr, elt);
VISIT(c, expr, val);
ADDOP_I(c, MAP_ADD, gen_index + 1);
ADDOP_I(c, MAP_ADD, depth + 1);
break;
default:
return 0;
Expand Down Expand Up @@ -4529,7 +4563,7 @@ compiler_comprehension(struct compiler *c, expr_ty e, int type,
ADDOP_I(c, op, 0);
}

if (!compiler_comprehension_generator(c, generators, 0, elt,
if (!compiler_comprehension_generator(c, generators, 0, 0, elt,
val, type))
goto error_in_scope;

Expand Down