Skip to content
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
154 changes: 154 additions & 0 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,160 @@ def or_false(x):
self.assertIn('LOAD_', opcodes[-2].opname)
self.assertEqual('RETURN_VALUE', opcodes[-1].opname)

def test_empty_set_unpack_literal_bytecode_optimization(self):
cases = {
# optimized cases
'{*()}': [
('RESUME', 0),
('BUILD_SET', 0),
('RETURN_VALUE', None),
],
'{*(), *()}': [
('RESUME', 0),
('BUILD_SET', 0),
('RETURN_VALUE', None),
],
'{*(), 1}': [
('RESUME', 0),
('LOAD_SMALL_INT', 1),
('BUILD_SET', 1),
('RETURN_VALUE', None),
],
'{*(), 1, 2, 3}': [
('RESUME', 0),
('BUILD_SET', 0),
('LOAD_CONST', frozenset({1, 2, 3})),
('SET_UPDATE', 1),
('RETURN_VALUE', None),
],
'{1, *()}': [
('RESUME', 0),
('LOAD_SMALL_INT', 1),
('BUILD_SET', 1),
('RETURN_VALUE', None),
],
'{1, 2, 3, *()}': [
('RESUME', 0),
('BUILD_SET', 0),
('LOAD_CONST', frozenset({1, 2, 3})),
('SET_UPDATE', 1),
('RETURN_VALUE', None),
],
'{1, 2, *(), 3}': [
('RESUME', 0),
('BUILD_SET', 0),
('LOAD_CONST', frozenset({1, 2, 3})),
('SET_UPDATE', 1),
('RETURN_VALUE', None),
],
# unoptimized cases
'{*(1,)}': [
('RESUME', 0),
('BUILD_SET', 0),
('LOAD_CONST', (1,)),
('SET_UPDATE', 1),
('RETURN_VALUE', None),
],
'{*(x,)}': [
('RESUME', 0),
('BUILD_SET', 0),
('LOAD_NAME', 'x'),
('BUILD_TUPLE', 1),
('SET_UPDATE', 1),
('RETURN_VALUE', None),
],
}

for source, expected in cases.items():
with self.subTest(source=source):
code = compile(source, '<test>', 'eval')
instructions = [
(instruction.opname, instruction.argval)
for instruction in dis.get_instructions(code)
]
self.assertEqual(instructions, expected)

def test_empty_leading_tuple_unpack_list_and_tuple_bytecode_optimization(self):
cases = {
# optimized cases
'[*()]': [
('RESUME', 0),
('BUILD_LIST', 0),
('RETURN_VALUE', None),
],
'[*(), *()]': [
('RESUME', 0),
('BUILD_LIST', 0),
('RETURN_VALUE', None),
],
'[*(), 1]': [
('RESUME', 0),
('LOAD_SMALL_INT', 1),
('BUILD_LIST', 1),
('RETURN_VALUE', None),
],
'(*(),)': [
('RESUME', 0),
('LOAD_COMMON_CONSTANT', ()),
('RETURN_VALUE', None),
],
'(*(), *())': [
('RESUME', 0),
('LOAD_COMMON_CONSTANT', ()),
('RETURN_VALUE', None),
],
'(*(), 1)': [
('RESUME', 0),
('LOAD_CONST', (1,)),
('RETURN_VALUE', None),
],
'[1, *()]': [
('RESUME', 0),
('LOAD_SMALL_INT', 1),
('BUILD_LIST', 1),
('RETURN_VALUE', None),
],
'[1, 2, 3, *()]': [
('RESUME', 0),
('BUILD_LIST', 0),
('LOAD_CONST', (1, 2, 3)),
('LIST_EXTEND', 1),
('RETURN_VALUE', None),
],
'[1, 2, *(), 3]': [
('RESUME', 0),
('BUILD_LIST', 0),
('LOAD_CONST', (1, 2, 3)),
('LIST_EXTEND', 1),
('RETURN_VALUE', None),
],
# unoptimized cases
'[*(1,)]': [
('RESUME', 0),
('BUILD_LIST', 0),
('LOAD_CONST', (1,)),
('LIST_EXTEND', 1),
('RETURN_VALUE', None),
],
'[*(x,)]': [
('RESUME', 0),
('BUILD_LIST', 0),
('LOAD_NAME', 'x'),
('BUILD_TUPLE', 1),
('LIST_EXTEND', 1),
('RETURN_VALUE', None),
],
}

for source, expected in cases.items():
with self.subTest(source=source):
code = compile(source, '<test>', 'eval')
instructions = [
(instruction.opname, instruction.argval)
for instruction in dis.get_instructions(code)
]
self.assertEqual(instructions, expected)

def test_imported_load_method(self):
sources = [
"""\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Optimize bytecode for empty unpack cases such as ``{*()}``.
42 changes: 35 additions & 7 deletions Python/codegen.c
Original file line number Diff line number Diff line change
Expand Up @@ -3412,24 +3412,46 @@ codegen_boolop(compiler *c, expr_ty e)
return SUCCESS;
}

static bool
is_empty_starred_literal(expr_ty elt)
{
if (elt->kind != Starred_kind) {
return false;
}
expr_ty value = elt->v.Starred.value;
return (value->kind == Tuple_kind &&
asdl_seq_LEN(value->v.Tuple.elts) == 0) ||
(value->kind == List_kind &&
asdl_seq_LEN(value->v.List.elts) == 0) ||
(value->kind == Dict_kind &&
asdl_seq_LEN(value->v.Dict.keys) == 0);
}

static int
starunpack_helper_impl(compiler *c, location loc,
asdl_expr_seq *elts, PyObject *injected_arg, int pushed,
int build, int add, int extend, int tuple)
{
Py_ssize_t n = asdl_seq_LEN(elts);
int big = n + pushed + (injected_arg ? 1 : 0) > _PY_STACK_USE_GUIDELINE;
Py_ssize_t end = asdl_seq_LEN(elts);
Py_ssize_t n = 0;
int seen_star = 0;
for (Py_ssize_t i = 0; i < n; i++) {
for (Py_ssize_t i = 0; i < end; i++) {
expr_ty elt = asdl_seq_GET(elts, i);
if (elt->kind == Starred_kind) {
if (is_empty_starred_literal(elt)) {
continue;
}
seen_star = 1;
break;
}
n++;
}
int big = n + pushed + (injected_arg ? 1 : 0) > _PY_STACK_USE_GUIDELINE;
if (!seen_star && !big) {
for (Py_ssize_t i = 0; i < n; i++) {
for (Py_ssize_t i = 0; i < end; i++) {
expr_ty elt = asdl_seq_GET(elts, i);
if (is_empty_starred_literal(elt)) {
continue;
}
VISIT(c, expr, elt);
}
if (injected_arg) {
Expand All @@ -3444,15 +3466,20 @@ starunpack_helper_impl(compiler *c, location loc,
return SUCCESS;
}
int sequence_built = 0;
Py_ssize_t nitems = 0;
if (big) {
ADDOP_I(c, loc, build, pushed);
sequence_built = 1;
}
for (Py_ssize_t i = 0; i < n; i++) {
for (Py_ssize_t i = 0; i < end; i++) {
expr_ty elt = asdl_seq_GET(elts, i);

if (elt->kind == Starred_kind) {
if (is_empty_starred_literal(elt)) {
continue;
}
if (sequence_built == 0) {
ADDOP_I(c, loc, build, i+pushed);
ADDOP_I(c, loc, build, nitems+pushed);
sequence_built = 1;
}
VISIT(c, expr, elt->v.Starred.value);
Expand All @@ -3464,6 +3491,7 @@ starunpack_helper_impl(compiler *c, location loc,
ADDOP_I(c, loc, add, 1);
}
}
nitems++;
}
assert(sequence_built);
if (injected_arg) {
Expand Down
Loading