Skip to content

Commit 12e77ac

Browse files
Eclips4iritkatriel
andauthored
[3.14] gh-148817: Fold long lists/sets of constant elements into constant tu… (#155898)
gh-148817: Fold long lists/sets of constant elements into constant tuples/frozensets (#149016) Fold long lists/sets of constant elements into constant tuples/frozensets. (cherry picked from commit 084230e) Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
1 parent 422e679 commit 12e77ac

4 files changed

Lines changed: 218 additions & 22 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Python/ceval*.h @markshannon
4848
Python/codegen.c @markshannon @iritkatriel
4949
Python/compile.c @markshannon @iritkatriel
5050
Python/assemble.c @markshannon @iritkatriel
51-
Python/flowgraph.c @markshannon @iritkatriel
51+
Python/flowgraph.c @markshannon @iritkatriel @eclips4
5252
Python/instruction_sequence.c @iritkatriel
5353
Python/bytecodes.c @markshannon
5454
Python/optimizer*.c @markshannon

Lib/test/test_peepholer.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2432,6 +2432,168 @@ def test_list_to_tuple_get_iter_is_safe(self):
24322432
self.assertEqual(b, [3, 2, 1, 0])
24332433
self.assertEqual(items, [])
24342434

2435+
def test_fold_constant_big_list_for_iter(self):
2436+
# for x in [c1, c2, ..., cN] (N > 30) should fold to LOAD_CONST tuple
2437+
consts = 35
2438+
before = (
2439+
[("BUILD_LIST", 0, 1)] +
2440+
[("LOAD_CONST", 0, 2), ("LIST_APPEND", 1, 3)] * consts +
2441+
[("GET_ITER", None, 4),
2442+
top := self.Label(),
2443+
("FOR_ITER", end := self.Label(), 5),
2444+
("STORE_FAST", 0, 6),
2445+
("JUMP", top, 7),
2446+
end,
2447+
("END_FOR", None, 8),
2448+
("POP_ITER", None, 9),
2449+
("LOAD_CONST", 0, 10),
2450+
("RETURN_VALUE", None, 11)]
2451+
)
2452+
after = [
2453+
("LOAD_CONST", 1, 3),
2454+
("GET_ITER", None, 4),
2455+
top := self.Label(),
2456+
("FOR_ITER", end := self.Label(), 5),
2457+
("STORE_FAST", 0, 6),
2458+
("JUMP", top, 7),
2459+
end,
2460+
("END_FOR", None, 8),
2461+
("POP_ITER", None, 9),
2462+
("LOAD_CONST", 0, 10),
2463+
("RETURN_VALUE", None, 11),
2464+
]
2465+
result_const = tuple(["test"] * consts)
2466+
self.cfg_optimization_test(before, after, consts=["test"],
2467+
expected_consts=["test", result_const])
2468+
2469+
def test_fold_constant_big_set_for_iter(self):
2470+
# for x in {c1, c2, ..., cN} (N > 30) should fold to LOAD_CONST frozenset
2471+
before = [
2472+
("BUILD_SET", 0, 1),
2473+
("LOAD_SMALL_INT", 1, 2), ("SET_ADD", 1, 3),
2474+
("LOAD_SMALL_INT", 2, 4), ("SET_ADD", 1, 5),
2475+
("LOAD_SMALL_INT", 3, 6), ("SET_ADD", 1, 7),
2476+
("GET_ITER", None, 8),
2477+
top := self.Label(),
2478+
("FOR_ITER", end := self.Label(), 9),
2479+
("STORE_FAST", 0, 10),
2480+
("JUMP", top, 11),
2481+
end,
2482+
("END_FOR", None, 12),
2483+
("POP_ITER", None, 13),
2484+
("LOAD_CONST", 0, 14),
2485+
("RETURN_VALUE", None, 15),
2486+
]
2487+
after = [
2488+
("LOAD_CONST", 1, 7),
2489+
("GET_ITER", None, 8),
2490+
top := self.Label(),
2491+
("FOR_ITER", end := self.Label(), 9),
2492+
("STORE_FAST", 0, 10),
2493+
("JUMP", top, 11),
2494+
end,
2495+
("END_FOR", None, 12),
2496+
("POP_ITER", None, 13),
2497+
("LOAD_CONST", 0, 14),
2498+
("RETURN_VALUE", None, 15),
2499+
]
2500+
self.cfg_optimization_test(before, after, consts=["test"],
2501+
expected_consts=["test", frozenset({1, 2, 3})])
2502+
2503+
def test_fold_constant_list_to_tuple_for_iter(self):
2504+
INTRINSIC_LIST_TO_TUPLE = 6
2505+
before = [
2506+
("BUILD_LIST", 0, 1),
2507+
("LOAD_SMALL_INT", 1, 2), ("LIST_APPEND", 1, 3),
2508+
("LOAD_SMALL_INT", 2, 4), ("LIST_APPEND", 1, 5),
2509+
("LOAD_SMALL_INT", 3, 6), ("LIST_APPEND", 1, 7),
2510+
("CALL_INTRINSIC_1", INTRINSIC_LIST_TO_TUPLE, 8),
2511+
("GET_ITER", None, 9),
2512+
top := self.Label(),
2513+
("FOR_ITER", end := self.Label(), 10),
2514+
("STORE_FAST", 0, 11),
2515+
("JUMP", top, 12),
2516+
end,
2517+
("END_FOR", None, 13),
2518+
("POP_ITER", None, 14),
2519+
("LOAD_CONST", 0, 15),
2520+
("RETURN_VALUE", None, 16),
2521+
]
2522+
after = [
2523+
("LOAD_CONST", 1, 8),
2524+
("GET_ITER", None, 9),
2525+
top := self.Label(),
2526+
("FOR_ITER", end := self.Label(), 10),
2527+
("STORE_FAST", 0, 11),
2528+
("JUMP", top, 12),
2529+
end,
2530+
("END_FOR", None, 13),
2531+
("POP_ITER", None, 14),
2532+
("LOAD_CONST", 0, 15),
2533+
("RETURN_VALUE", None, 16),
2534+
]
2535+
self.cfg_optimization_test(before, after, consts=["test"],
2536+
expected_consts=["test", (1, 2, 3)])
2537+
2538+
def test_fold_constant_big_list_contains_op(self):
2539+
# x in [c1, c2, ..., cN] (N > 30) should fold to LOAD_CONST tuple
2540+
before = [
2541+
("LOAD_FAST", 0, 1),
2542+
("BUILD_LIST", 0, 2),
2543+
("LOAD_SMALL_INT", 1, 3), ("LIST_APPEND", 1, 4),
2544+
("LOAD_SMALL_INT", 2, 5), ("LIST_APPEND", 1, 6),
2545+
("LOAD_SMALL_INT", 3, 7), ("LIST_APPEND", 1, 8),
2546+
("CONTAINS_OP", 0, 9),
2547+
("RETURN_VALUE", None, 10),
2548+
]
2549+
after = [
2550+
("LOAD_FAST_BORROW", 0, 1),
2551+
("LOAD_CONST", 1, 8),
2552+
("CONTAINS_OP", 0, 9),
2553+
("RETURN_VALUE", None, 10),
2554+
]
2555+
self.cfg_optimization_test(before, after, consts=[None],
2556+
expected_consts=[None, (1, 2, 3)])
2557+
2558+
def test_fold_constant_big_set_contains_op(self):
2559+
# x in {c1, c2, ..., cN} (N > 30) should fold to LOAD_CONST frozenset
2560+
before = [
2561+
("LOAD_FAST", 0, 1),
2562+
("BUILD_SET", 0, 2),
2563+
("LOAD_SMALL_INT", 1, 3), ("SET_ADD", 1, 4),
2564+
("LOAD_SMALL_INT", 2, 5), ("SET_ADD", 1, 6),
2565+
("LOAD_SMALL_INT", 3, 7), ("SET_ADD", 1, 8),
2566+
("CONTAINS_OP", 0, 9),
2567+
("RETURN_VALUE", None, 10),
2568+
]
2569+
after = [
2570+
("LOAD_FAST_BORROW", 0, 1),
2571+
("LOAD_CONST", 1, 8),
2572+
("CONTAINS_OP", 0, 9),
2573+
("RETURN_VALUE", None, 10),
2574+
]
2575+
self.cfg_optimization_test(before, after, consts=[None],
2576+
expected_consts=[None, frozenset({1, 2, 3})])
2577+
2578+
def test_no_fold_big_list_for_iter_with_non_const(self):
2579+
same = [
2580+
("BUILD_LIST", 0, 1),
2581+
("LOAD_SMALL_INT", 1, 2), ("LIST_APPEND", 1, 3),
2582+
("LOAD_FAST_BORROW", 0, 4), ("LIST_APPEND", 1, 5),
2583+
("LOAD_SMALL_INT", 3, 6), ("LIST_APPEND", 1, 7),
2584+
("GET_ITER", None, 8),
2585+
top := self.Label(),
2586+
("FOR_ITER", end := self.Label(), 9),
2587+
("STORE_FAST", 1, 10),
2588+
("JUMP", top, 11),
2589+
end,
2590+
("END_FOR", None, 12),
2591+
("POP_ITER", None, 13),
2592+
("LOAD_CONST", 0, 14),
2593+
("RETURN_VALUE", None, 15),
2594+
]
2595+
self.cfg_optimization_test(same, same, consts=["test"])
2596+
24352597

24362598
class OptimizeLoadFastTestCase(DirectCfgOptimizerTests):
24372599
def make_bb(self, insts):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fold large constant list and set literals used as the iterable of a
2+
:keyword:`for` loop or ``in``/``not in`` test into a constant
3+
:class:`tuple` or :class:`frozenset`, restoring an optimization
4+
previously done by the AST optimizer that was lost when constant
5+
folding moved to the CFG.

Python/flowgraph.c

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,34 +1492,48 @@ fold_tuple_of_constants(basicblock *bb, int i, PyObject *consts,
14921492
}
14931493

14941494
/* Replace:
1495-
BUILD_LIST 0
1495+
BUILD_LIST/BUILD_SET 0
14961496
LOAD_CONST c1
1497-
LIST_APPEND 1
1497+
LIST_APPEND/SET_ADD 1
14981498
LOAD_CONST c2
1499-
LIST_APPEND 1
1499+
LIST_APPEND/SET_ADD 1
15001500
...
15011501
LOAD_CONST cN
1502-
LIST_APPEND 1
1503-
CALL_INTRINSIC_1 INTRINSIC_LIST_TO_TUPLE
1502+
LIST_APPEND/SET_ADD 1
1503+
[CALL_INTRINSIC_1 INTRINSIC_LIST_TO_TUPLE] <-- optional
15041504
with:
15051505
LOAD_CONST (c1, c2, ... cN)
1506+
The instruction at `i` is either the LIST_TO_TUPLE intrinsic (so the
1507+
immediately preceding non-NOP instruction is expected to be a
1508+
LIST_APPEND, and only the BUILD_LIST/LIST_APPEND form is considered),
1509+
or the trailing LIST_APPEND or SET_ADD itself, in which case the
1510+
matching BUILD_LIST/BUILD_SET start is selected from its opcode, and
1511+
for sets the result is wrapped in a frozenset.
15061512
*/
15071513
static int
1508-
fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
1509-
PyObject *consts, PyObject *const_cache,
1510-
_Py_hashtable_t *consts_index)
1514+
fold_constant_seq_into_load_const(basicblock *bb, int i,
1515+
PyObject *consts, PyObject *const_cache,
1516+
_Py_hashtable_t *consts_index)
15111517
{
15121518
assert(PyDict_CheckExact(const_cache));
15131519
assert(PyList_CheckExact(consts));
15141520
assert(i >= 0);
15151521
assert(i < bb->b_iused);
15161522

1517-
cfg_instr *intrinsic = &bb->b_instr[i];
1518-
assert(intrinsic->i_opcode == CALL_INTRINSIC_1);
1519-
assert(intrinsic->i_oparg == INTRINSIC_LIST_TO_TUPLE);
1520-
1523+
cfg_instr *target = &bb->b_instr[i];
1524+
assert(target->i_opcode == LIST_APPEND || target->i_opcode == SET_ADD ||
1525+
(target->i_opcode == CALL_INTRINSIC_1 &&
1526+
target->i_oparg == INTRINSIC_LIST_TO_TUPLE));
1527+
bool expected_append = target->i_opcode == CALL_INTRINSIC_1;
1528+
int append_op = expected_append ? LIST_APPEND : target->i_opcode;
1529+
assert(append_op == LIST_APPEND || append_op == SET_ADD);
1530+
int build_op = append_op == LIST_APPEND ? BUILD_LIST : BUILD_SET;
15211531
int consts_found = 0;
1522-
bool expect_append = true;
1532+
/* Walking backward from `i`, we expect LIST_APPEND/SET_ADD and
1533+
LOAD_CONST to alternate. If `i` is the trailing LIST_TO_TUPLE
1534+
intrinsic, the next instruction back is an APPEND. If `i` is the
1535+
trailing APPEND itself, the next instruction back is a LOAD_CONST. */
1536+
bool expect_append = expected_append;
15231537

15241538
for (int pos = i - 1; pos >= 0; pos--) {
15251539
cfg_instr *instr = &bb->b_instr[pos];
@@ -1530,7 +1544,7 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
15301544
continue;
15311545
}
15321546

1533-
if (opcode == BUILD_LIST && oparg == 0) {
1547+
if (opcode == build_op && oparg == 0) {
15341548
if (!expect_append) {
15351549
/* Not a sequence start. */
15361550
return SUCCESS;
@@ -1542,7 +1556,8 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
15421556
return ERROR;
15431557
}
15441558

1545-
for (int newpos = i - 1; newpos >= pos; newpos--) {
1559+
int newpos_start = expected_append ? i - 1 : i;
1560+
for (int newpos = newpos_start; newpos >= pos; newpos--) {
15461561
instr = &bb->b_instr[newpos];
15471562
if (instr->i_opcode == NOP) {
15481563
continue;
@@ -1559,11 +1574,20 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i,
15591574
nop_out(&instr, 1);
15601575
}
15611576
assert(consts_found == 0);
1562-
return instr_make_load_const(intrinsic, newconst, consts, const_cache, consts_index);
1577+
1578+
if (build_op == BUILD_SET) {
1579+
PyObject *frozen = PyFrozenSet_New(newconst);
1580+
Py_DECREF(newconst);
1581+
if (frozen == NULL) {
1582+
return ERROR;
1583+
}
1584+
newconst = frozen;
1585+
}
1586+
return instr_make_load_const(target, newconst, consts, const_cache, consts_index);
15631587
}
15641588

15651589
if (expect_append) {
1566-
if (opcode != LIST_APPEND || oparg != 1) {
1590+
if (opcode != append_op || oparg != 1) {
15671591
return SUCCESS;
15681592
}
15691593
}
@@ -2491,17 +2515,22 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts,
24912515
break;
24922516
case CALL_INTRINSIC_1:
24932517
if (oparg == INTRINSIC_LIST_TO_TUPLE) {
2494-
if (nextop == GET_ITER) {
2518+
RETURN_IF_ERROR(fold_constant_seq_into_load_const(bb, i, consts, const_cache, consts_index));
2519+
if (inst->i_opcode == CALL_INTRINSIC_1 && nextop == GET_ITER) {
24952520
INSTR_SET_OP0(inst, NOP);
24962521
}
2497-
else {
2498-
RETURN_IF_ERROR(fold_constant_intrinsic_list_to_tuple(bb, i, consts, const_cache, consts_index));
2499-
}
25002522
}
25012523
else if (oparg == INTRINSIC_UNARY_POSITIVE) {
25022524
RETURN_IF_ERROR(fold_const_unaryop(bb, i, consts, const_cache, consts_index));
25032525
}
25042526
break;
2527+
case LIST_APPEND:
2528+
case SET_ADD:
2529+
if (oparg == 1 && (nextop == GET_ITER || nextop == CONTAINS_OP)) {
2530+
RETURN_IF_ERROR(fold_constant_seq_into_load_const(
2531+
bb, i, consts, const_cache, consts_index));
2532+
}
2533+
break;
25052534
case BINARY_OP:
25062535
RETURN_IF_ERROR(fold_const_binop(bb, i, consts, const_cache, consts_index));
25072536
break;

0 commit comments

Comments
 (0)