Skip to content

Commit e4a6855

Browse files
committed
add opcode for more efficient comprehension execution
1 parent f02fa64 commit e4a6855

File tree

17 files changed

+165
-78
lines changed

17 files changed

+165
-78
lines changed

Doc/library/dis.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,20 @@ iterations of the loop.
13041304
* 2: ``raise STACK[-2] from STACK[-1]`` (raise exception instance or type at
13051305
``STACK[-2]`` with ``__cause__`` set to ``STACK[-1]``)
13061306

1307+
.. opcode:: COMPREHENSION (flag)
1308+
1309+
Calls a comprehension code object, without creating and throwing away a
1310+
single-use function object. ``flag`` must be either ``0`` or ``1``, the
1311+
latter indicating the comprehension has free variables and a closure tuple
1312+
will be on the stack.
1313+
1314+
The stack should contain, from bottom to top:
1315+
1316+
* a tuple containing cells for free variables, if ``flag`` is set
1317+
* the code object for the comprehension
1318+
* the single "argument" to the comprehension (the iterated object)
1319+
1320+
.. versionadded:: 3.12
13071321

13081322
.. opcode:: CALL (argc)
13091323

Include/internal/pycore_frame.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ typedef struct _PyInterpreterFrame {
5252
PyObject *f_globals; /* Borrowed reference. Only valid if not on C stack */
5353
PyObject *f_builtins; /* Borrowed reference. Only valid if not on C stack */
5454
PyObject *f_locals; /* Strong reference, may be NULL. Only valid if not on C stack */
55+
// For comprehensions, f_closure and f_code may not match func_closure and
56+
// func_code from f_funcobj above; f_funcobj will be the calling function.
57+
PyObject *f_closure; /* Strong reference, may be NULL. Only valid if not on C stack */
5558
PyCodeObject *f_code; /* Strong reference */
5659
PyFrameObject *frame_obj; /* Strong reference, may be NULL. Only valid if not on C stack */
5760
/* Linkage section */
@@ -112,12 +115,14 @@ void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *dest);
112115
static inline void
113116
_PyFrame_Initialize(
114117
_PyInterpreterFrame *frame, PyFunctionObject *func,
115-
PyObject *locals, PyCodeObject *code, int null_locals_from)
118+
PyObject *locals, PyCodeObject *code, PyObject *closure,
119+
int null_locals_from)
116120
{
117121
frame->f_funcobj = (PyObject *)func;
118122
frame->f_code = (PyCodeObject *)Py_NewRef(code);
119123
frame->f_builtins = func->func_builtins;
120124
frame->f_globals = func->func_globals;
125+
frame->f_closure = Py_XNewRef(closure);
121126
frame->f_locals = locals;
122127
frame->stacktop = code->co_nlocalsplus;
123128
frame->frame_obj = NULL;
@@ -250,7 +255,7 @@ _PyFrame_PushUnchecked(PyThreadState *tstate, PyFunctionObject *func, int null_l
250255
_PyInterpreterFrame *new_frame = (_PyInterpreterFrame *)tstate->datastack_top;
251256
tstate->datastack_top += code->co_framesize;
252257
assert(tstate->datastack_top < tstate->datastack_limit);
253-
_PyFrame_Initialize(new_frame, func, NULL, code, null_locals_from);
258+
_PyFrame_Initialize(new_frame, func, NULL, code, func->func_closure, null_locals_from);
254259
return new_frame;
255260
}
256261

Include/internal/pycore_opcode.h

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/opcode.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ def _write_atomic(path, data, mode=0o666):
431431
# Python 3.12a1 3515 (Embed jump mask in COMPARE_OP oparg)
432432
# Python 3.12a1 3516 (Add COMPARE_AND_BRANCH instruction)
433433
# Python 3.12a1 3517 (Change YIELD_VALUE oparg to exception block depth)
434+
# Python 3.12a1 3518 (Add COMPREHENSION instruction)
434435

435436
# Python 3.13 will start with 3550
436437

@@ -443,7 +444,7 @@ def _write_atomic(path, data, mode=0o666):
443444
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
444445
# in PC/launcher.c must also be updated.
445446

446-
MAGIC_NUMBER = (3517).to_bytes(2, 'little') + b'\r\n'
447+
MAGIC_NUMBER = (3518).to_bytes(2, 'little') + b'\r\n'
447448

448449
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
449450

Lib/opcode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ def pseudo_op(name, op, real_ops):
218218
def_op('DICT_MERGE', 164)
219219
def_op('DICT_UPDATE', 165)
220220

221+
def_op('COMPREHENSION', 170)
221222
def_op('CALL', 171)
222223
def_op('KW_NAMES', 172)
223224
hasconst.append(172)

Lib/test/test_dis.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,9 @@ def bug1333982(x=[]):
170170
171171
%3d LOAD_ASSERTION_ERROR
172172
LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
173-
MAKE_FUNCTION 0
174173
LOAD_FAST 0 (x)
175174
GET_ITER
176-
CALL 0
175+
COMPREHENSION 0
177176
178177
%3d LOAD_CONST 2 (1)
179178
@@ -675,10 +674,9 @@ def foo(x):
675674
%3d LOAD_CLOSURE 0 (x)
676675
BUILD_TUPLE 1
677676
LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
678-
MAKE_FUNCTION 8 (closure)
679677
LOAD_DEREF 1 (y)
680678
GET_ITER
681-
CALL 0
679+
COMPREHENSION 1
682680
RETURN_VALUE
683681
""" % (dis_nested_0,
684682
__file__,

Lib/test/test_sys.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,7 +1443,7 @@ class C(object): pass
14431443
def func():
14441444
return sys._getframe()
14451445
x = func()
1446-
check(x, size('3Pi3c7P2ic??2P'))
1446+
check(x, size('3Pi3c8P2ic??2P'))
14471447
# function
14481448
def func(): pass
14491449
check(func, size('14Pi'))
@@ -1460,7 +1460,7 @@ def bar(cls):
14601460
check(bar, size('PP'))
14611461
# generator
14621462
def get_gen(): yield 1
1463-
check(get_gen(), size('P2P4P4c7P2ic??2P'))
1463+
check(get_gen(), size('P2P4P4c8P2ic??2P'))
14641464
# iterator
14651465
check(iter('abc'), size('lP'))
14661466
# callable-iterator

Objects/frame_layout.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ The specials sections contains the following pointers:
7575
* Builtins dict
7676
* Locals dict (not the "fast" locals, but the locals for eval and class creation)
7777
* Code object
78+
* Closure tuple of cells for free variables, if any.
7879
* Heap allocated `PyFrameObject` for this activation record, if any.
7980
* The function.
8081

Objects/frameobject.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,7 @@ frame_dealloc(PyFrameObject *f)
882882
frame->f_code = NULL;
883883
Py_CLEAR(frame->f_funcobj);
884884
Py_CLEAR(frame->f_locals);
885+
Py_CLEAR(frame->f_closure);
885886
PyObject **locals = _PyFrame_GetLocalsArray(frame);
886887
for (int i = 0; i < frame->stacktop; i++) {
887888
Py_CLEAR(locals[i]);
@@ -1020,7 +1021,7 @@ init_frame(_PyInterpreterFrame *frame, PyFunctionObject *func, PyObject *locals)
10201021
{
10211022
PyCodeObject *code = (PyCodeObject *)func->func_code;
10221023
_PyFrame_Initialize(frame, (PyFunctionObject*)Py_NewRef(func),
1023-
Py_XNewRef(locals), code, 0);
1024+
Py_XNewRef(locals), code, func->func_closure, 0);
10241025
frame->previous = NULL;
10251026
}
10261027

@@ -1123,7 +1124,7 @@ frame_init_get_vars(_PyInterpreterFrame *frame)
11231124
}
11241125

11251126
/* Free vars have not been initialized -- Do that */
1126-
PyObject *closure = ((PyFunctionObject *)frame->f_funcobj)->func_closure;
1127+
PyObject *closure = frame->f_closure;
11271128
int offset = PyCode_GetFirstFree(co);
11281129
for (int i = 0; i < co->co_nfreevars; ++i) {
11291130
PyObject *o = PyTuple_GET_ITEM(closure, i);

Python/bytecodes.c

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,8 +1265,7 @@ dummy_func(
12651265
inst(COPY_FREE_VARS, (--)) {
12661266
/* Copy closure variables to free variables */
12671267
PyCodeObject *co = frame->f_code;
1268-
assert(PyFunction_Check(frame->f_funcobj));
1269-
PyObject *closure = ((PyFunctionObject *)frame->f_funcobj)->func_closure;
1268+
PyObject *closure = frame->f_closure;
12701269
assert(oparg == co->co_nfreevars);
12711270
int offset = co->co_nlocalsplus - oparg;
12721271
for (int i = 0; i < oparg; ++i) {
@@ -2515,6 +2514,23 @@ dummy_func(
25152514
kwnames = GETITEM(consts, oparg);
25162515
}
25172516

2517+
// stack effect: (__0, __1, __array[oparg] --)
2518+
inst(COMPREHENSION) {
2519+
PyObject *code = PEEK(2);
2520+
PyObject *closure = oparg ? PEEK(3) : NULL;
2521+
_PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit(
2522+
tstate, Py_NewRef(frame->f_funcobj), code, closure, NULL,
2523+
stack_pointer - 1, 1, NULL
2524+
);
2525+
Py_XDECREF(code);
2526+
Py_XDECREF(closure);
2527+
STACK_SHRINK(oparg + 2);
2528+
if (new_frame == NULL) {
2529+
goto error;
2530+
}
2531+
DISPATCH_INLINED(new_frame);
2532+
}
2533+
25182534
// stack effect: (__0, __array[oparg] -- )
25192535
inst(CALL) {
25202536
#if ENABLE_SPECIALIZATION
@@ -2553,8 +2569,9 @@ dummy_func(
25532569
int code_flags = ((PyCodeObject*)PyFunction_GET_CODE(function))->co_flags;
25542570
PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : Py_NewRef(PyFunction_GET_GLOBALS(function));
25552571
STACK_SHRINK(total_args);
2572+
PyFunctionObject *func = (PyFunctionObject *)function;
25562573
_PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit(
2557-
tstate, (PyFunctionObject *)function, locals,
2574+
tstate, func, func->func_code, func->func_closure, locals,
25582575
stack_pointer, positional_args, kwnames
25592576
);
25602577
kwnames = NULL;

Python/ceval.c

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ static void format_awaitable_error(PyThreadState *, PyTypeObject *, int);
210210
static int get_exception_handler(PyCodeObject *, int, int*, int*, int*);
211211
static _PyInterpreterFrame *
212212
_PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func,
213-
PyObject *locals, PyObject* const* args,
214-
size_t argcount, PyObject *kwnames);
213+
PyObject *code, PyObject *closure, PyObject *locals,
214+
PyObject* const* args, size_t argcount, PyObject *kwnames);
215215
static void
216216
_PyEvalFrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame);
217217

@@ -752,6 +752,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
752752
entry_frame.frame_obj = (PyFrameObject*)0xaaa2;
753753
entry_frame.f_globals = (PyObject*)0xaaa3;
754754
entry_frame.f_builtins = (PyObject*)0xaaa4;
755+
entry_frame.f_closure = (PyObject*)0xaaa5;
755756
#endif
756757
entry_frame.f_code = tstate->interp->interpreter_trampoline;
757758
entry_frame.prev_instr =
@@ -1388,10 +1389,9 @@ get_exception_handler(PyCodeObject *code, int index, int *level, int *handler, i
13881389

13891390
static int
13901391
initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
1391-
PyObject **localsplus, PyObject *const *args,
1392+
PyCodeObject *co, PyObject **localsplus, PyObject *const *args,
13921393
Py_ssize_t argcount, PyObject *kwnames)
13931394
{
1394-
PyCodeObject *co = (PyCodeObject*)func->func_code;
13951395
const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount;
13961396

13971397
/* Create a dictionary for keyword parameters (**kwags) */
@@ -1613,18 +1613,19 @@ initialize_locals(PyThreadState *tstate, PyFunctionObject *func,
16131613
/* Consumes references to func, locals and all the args */
16141614
static _PyInterpreterFrame *
16151615
_PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func,
1616-
PyObject *locals, PyObject* const* args,
1617-
size_t argcount, PyObject *kwnames)
1616+
PyObject *codeobj, PyObject *closure, PyObject *locals,
1617+
PyObject* const* args, size_t argcount, PyObject *kwnames)
16181618
{
1619-
PyCodeObject * code = (PyCodeObject *)func->func_code;
16201619
CALL_STAT_INC(frames_pushed);
1620+
assert(PyCode_Check(codeobj));
1621+
PyCodeObject *code = (PyCodeObject *)codeobj;
16211622
_PyInterpreterFrame *frame = _PyThreadState_PushFrame(tstate, code->co_framesize);
16221623
if (frame == NULL) {
16231624
goto fail;
16241625
}
1625-
_PyFrame_Initialize(frame, func, locals, code, 0);
1626+
_PyFrame_Initialize(frame, func, locals, code, closure, 0);
16261627
PyObject **localsarray = &frame->localsplus[0];
1627-
if (initialize_locals(tstate, func, localsarray, args, argcount, kwnames)) {
1628+
if (initialize_locals(tstate, func, code, localsarray, args, argcount, kwnames)) {
16281629
assert(frame->owner != FRAME_OWNED_BY_GENERATOR);
16291630
_PyEvalFrameClearAndPop(tstate, frame);
16301631
return NULL;
@@ -1708,7 +1709,8 @@ _PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
17081709
}
17091710
}
17101711
_PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
1711-
tstate, func, locals, args, argcount, kwnames);
1712+
tstate, func, func->func_code, func->func_closure, locals,
1713+
args, argcount, kwnames);
17121714
if (frame == NULL) {
17131715
return NULL;
17141716
}

0 commit comments

Comments
 (0)