Skip to content

gh-131738: optimize builtin any/all/tuple calls with a generator expression arg #131737

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
merged 17 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ struct _Py_global_strings {
STRUCT_FOR_ID(aggregate_class)
STRUCT_FOR_ID(alias)
STRUCT_FOR_ID(align)
STRUCT_FOR_ID(all)
STRUCT_FOR_ID(allow_code)
STRUCT_FOR_ID(any)
STRUCT_FOR_ID(append)
STRUCT_FOR_ID(arg)
STRUCT_FOR_ID(argdefs)
Expand Down
3 changes: 2 additions & 1 deletion Include/internal/pycore_magic_number.h
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Known values:
Python 3.14a6 3617 (Branch monitoring for async for loops)
Python 3.14a6 3618 (Add oparg to END_ASYNC_FOR)
Python 3.14a6 3619 (Renumber RESUME opcode from 149 to 128)
Python 3.14a6 3620 (Optimize bytecode for all/any/tuple called on a genexp)

Python 3.15 will start with 3650

Expand All @@ -284,7 +285,7 @@ PC/launcher.c must also be updated.

*/

#define PYC_MAGIC_NUMBER 3619
#define PYC_MAGIC_NUMBER 3620
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
(little-endian) and then appending b'\r\n'. */
#define PYC_MAGIC_NUMBER_TOKEN \
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_opcode_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Include/internal/pycore_opcode_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ extern "C" {
/* Values used as the oparg for LOAD_COMMON_CONSTANT */
#define CONSTANT_ASSERTIONERROR 0
#define CONSTANT_NOTIMPLEMENTEDERROR 1
#define NUM_COMMON_CONSTANTS 2
#define CONSTANT_BUILTIN_TUPLE 2
#define CONSTANT_BUILTIN_ALL 3
#define CONSTANT_BUILTIN_ANY 4
#define NUM_COMMON_CONSTANTS 5

/* Values used in the oparg for RESUME */
#define RESUME_AT_FUNC_START 0
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Include/internal/pycore_uop_metadata.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"HAVE_ARGUMENT", "EXTENDED_ARG", "hasarg", "hasconst", "hasname",
"hasjump", "hasjrel", "hasjabs", "hasfree", "haslocal", "hasexc"]

import builtins
import _opcode
from _opcode import stack_effect

Expand Down Expand Up @@ -38,7 +39,8 @@
_intrinsic_1_descs = _opcode.get_intrinsic1_descs()
_intrinsic_2_descs = _opcode.get_intrinsic2_descs()
_special_method_names = _opcode.get_special_method_names()
_common_constants = [AssertionError, NotImplementedError]
_common_constants = [AssertionError, NotImplementedError,
builtins.tuple, builtins.all, builtins.any]
_nb_ops = _opcode.get_nb_ops()

hascompare = [opmap["COMPARE_OP"]]
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ def test_all(self):
self.assertEqual(all(x > 42 for x in S), True)
S = [50, 40, 60]
self.assertEqual(all(x > 42 for x in S), False)
S = [50, 40, 60, TestFailingBool()]
self.assertEqual(all(x > 42 for x in S), False)

def test_any(self):
self.assertEqual(any([None, None, None]), False)
Expand All @@ -238,9 +240,49 @@ def test_any(self):
self.assertEqual(any([1, TestFailingBool()]), True) # Short-circuit
S = [40, 60, 30]
self.assertEqual(any(x > 42 for x in S), True)
S = [40, 60, 30, TestFailingBool()]
self.assertEqual(any(x > 42 for x in S), True)
S = [10, 20, 30]
self.assertEqual(any(x > 42 for x in S), False)

def test_all_any_tuple_optimization(self):
def f_all():
return all(x-2 for x in [1,2,3])

def f_any():
return any(x-1 for x in [1,2,3])

def f_tuple():
return tuple(2*x for x in [1,2,3])

funcs = [f_all, f_any, f_tuple]

for f in funcs:
# check that generator code object is not duplicated
code_objs = [c for c in f.__code__.co_consts if isinstance(c, type(f.__code__))]
self.assertEqual(len(code_objs), 1)


# check the overriding the builtins works
bltin_outputs = [f() for f in funcs]

global all, any, tuple
saved = all, any, tuple
try:
all = lambda x : "all"
any = lambda x : "any"
tuple = lambda x : "tuple"

return [f() for f in funcs]
finally:
all, any, tuple = saved

overridden_outputs = run_with_overrides()

for f, out1, out2 in zip(funcs, bltin_outputs, overridden_outputs):
with self.subTest(func = f.__name__):
self.assertNotEqual(out1, out2)

def test_ascii(self):
self.assertEqual(ascii(''), '\'\'')
self.assertEqual(ascii(0), '0')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Compiler emits optimised code for builtin any/all/tuple calls over a generator expression.
17 changes: 14 additions & 3 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1409,11 +1409,22 @@ dummy_func(
if (oparg == CONSTANT_ASSERTIONERROR) {
val = PyExc_AssertionError;
}
else {
assert(oparg == CONSTANT_NOTIMPLEMENTEDERROR);
else if (oparg == CONSTANT_NOTIMPLEMENTEDERROR) {
val = PyExc_NotImplementedError;
}
value = PyStackRef_FromPyObjectImmortal(val);
else if (oparg == CONSTANT_BUILTIN_TUPLE) {
val = (PyObject*)&PyTuple_Type;
}
else if (oparg == CONSTANT_BUILTIN_ALL) {
val = PyDict_GetItemWithError(BUILTINS(), &_Py_ID(all));
}
else if (oparg == CONSTANT_BUILTIN_ANY) {
val = PyDict_GetItemWithError(BUILTINS(), &_Py_ID(any));
}
else {
Py_UNREACHABLE();
}
value = PyStackRef_FromPyObjectNew(val);
}

inst(LOAD_BUILD_CLASS, ( -- bc)) {
Expand Down
101 changes: 98 additions & 3 deletions Python/codegen.c
Original file line number Diff line number Diff line change
Expand Up @@ -3723,6 +3723,97 @@ update_start_location_to_match_attr(compiler *c, location loc,
return loc;
}

static int
maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
{
asdl_expr_seq *args = e->v.Call.args;
asdl_keyword_seq *kwds = e->v.Call.keywords;
expr_ty func = e->v.Call.func;

if (! (func->kind == Name_kind &&
asdl_seq_LEN(args) == 1 &&
asdl_seq_LEN(kwds) == 0 &&
asdl_seq_GET(args, 0)->kind == GeneratorExp_kind))
{
return 0;
}

expr_ty generator_exp = asdl_seq_GET(args, 0);

if (asdl_seq_LEN(generator_exp->v.GeneratorExp.generators) != 1) {
return 0;
}

location loc = LOC(func);

int optimized = 0;
NEW_JUMP_TARGET_LABEL(c, skip_optimization);

int const_oparg = -1;
PyObject *initial_res = NULL;
int continue_jump_opcode = -1;
if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "all")) {
const_oparg = CONSTANT_BUILTIN_ALL;
initial_res = Py_True;
continue_jump_opcode = POP_JUMP_IF_TRUE;
}
else if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "any")) {
const_oparg = CONSTANT_BUILTIN_ANY;
initial_res = Py_False;
continue_jump_opcode = POP_JUMP_IF_FALSE;
}
else if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "tuple")) {
const_oparg = CONSTANT_BUILTIN_TUPLE;
}
if (const_oparg != -1) {
RETURN_IF_ERROR(codegen_nameop(c, loc, func->v.Name.id, Load));
ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, const_oparg);
ADDOP_COMPARE(c, loc, Is);
ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, skip_optimization);

if (const_oparg == CONSTANT_BUILTIN_TUPLE) {
ADDOP_I(c, loc, BUILD_LIST, 0);
}
else {
ADDOP_LOAD_CONST(c, loc, initial_res);
}
VISIT(c, expr, generator_exp);

NEW_JUMP_TARGET_LABEL(c, loop);
NEW_JUMP_TARGET_LABEL(c, cleanup);

USE_LABEL(c, loop);
ADDOP_JUMP(c, loc, FOR_ITER, cleanup);
if (const_oparg == CONSTANT_BUILTIN_TUPLE) {
ADDOP_I(c, loc, LIST_APPEND, 2);
ADDOP_JUMP(c, loc, JUMP, loop);
}
else {
ADDOP(c, loc, TO_BOOL);
ADDOP_JUMP(c, loc, continue_jump_opcode, loop);
}

ADDOP(c, NO_LOCATION, POP_ITER);
if (const_oparg != CONSTANT_BUILTIN_TUPLE) {
ADDOP(c, loc, POP_TOP);
ADDOP_LOAD_CONST(c, loc, initial_res == Py_True ? Py_False : Py_True);
}
ADDOP_JUMP(c, loc, JUMP, end);

USE_LABEL(c, cleanup);
ADDOP(c, NO_LOCATION, END_FOR);
ADDOP(c, NO_LOCATION, POP_ITER);
if (const_oparg == CONSTANT_BUILTIN_TUPLE) {
ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_LIST_TO_TUPLE);
}

optimized = 1;
ADDOP_JUMP(c, loc, JUMP, end);
}
USE_LABEL(c, skip_optimization);
return optimized;
}

// Return 1 if the method call was optimized, 0 if not, and -1 on error.
static int
maybe_optimize_method_call(compiler *c, expr_ty e)
Expand Down Expand Up @@ -3829,14 +3920,18 @@ codegen_call(compiler *c, expr_ty e)
if (ret == 1) {
return SUCCESS;
}
NEW_JUMP_TARGET_LABEL(c, skip_normal_call);
RETURN_IF_ERROR(maybe_optimize_function_call(c, e, skip_normal_call));
RETURN_IF_ERROR(check_caller(c, e->v.Call.func));
VISIT(c, expr, e->v.Call.func);
location loc = LOC(e->v.Call.func);
ADDOP(c, loc, PUSH_NULL);
loc = LOC(e);
return codegen_call_helper(c, loc, 0,
e->v.Call.args,
e->v.Call.keywords);
ret = codegen_call_helper(c, loc, 0,
e->v.Call.args,
e->v.Call.keywords);
USE_LABEL(c, skip_normal_call);
return ret;
}

static int
Expand Down
29 changes: 26 additions & 3 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 additions & 3 deletions Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading