Skip to content

Commit

Permalink
bpo-44525: Copy free variables in bytecode to allow calls to inner fu…
Browse files Browse the repository at this point in the history
…nctions to be specialized (GH-29595)

* Make internal APIs that take PyFrameConstructor take a PyFunctionObject instead.

* Add reference to function to frame, borrow references to builtins and globals.

* Add COPY_FREE_VARS instruction to allow specialization of calls to inner functions.
  • Loading branch information
markshannon authored Nov 23, 2021
1 parent d82f2ca commit 135cabd
Show file tree
Hide file tree
Showing 25 changed files with 269 additions and 157 deletions.
9 changes: 9 additions & 0 deletions Doc/library/dis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,15 @@ All of the following opcodes use their arguments.
``i`` is no longer offset by the length of ``co_varnames``.


.. opcode:: COPY_FREE_VARS (n)

Copies the ``n`` free variables from the closure into the frame.
Removes the need for special code on the caller's side when calling
closures.

.. versionadded:: 3.11


.. opcode:: RAISE_VARARGS (argc)

Raises an exception using one of the 3 forms of the ``raise`` statement,
Expand Down
3 changes: 0 additions & 3 deletions Include/cpython/funcobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,6 @@ uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func);
#define PyFunction_GET_ANNOTATIONS(func) \
(((PyFunctionObject *)func) -> func_annotations)

#define PyFunction_AS_FRAME_CONSTRUCTOR(func) \
((PyFrameConstructor *)&((PyFunctionObject *)(func))->func_globals)

/* The classmethod and staticmethod types lives here, too */
PyAPI_DATA(PyTypeObject) PyClassMethod_Type;
PyAPI_DATA(PyTypeObject) PyStaticMethod_Type;
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ _PyEval_EvalFrame(PyThreadState *tstate, struct _interpreter_frame *frame, int t

extern PyObject *
_PyEval_Vector(PyThreadState *tstate,
PyFrameConstructor *desc, PyObject *locals,
PyFunctionObject *func, PyObject *locals,
PyObject* const* args, size_t argcount,
PyObject *kwnames);

Expand Down Expand Up @@ -113,7 +113,7 @@ static inline void _Py_LeaveRecursiveCall_inline(void) {

struct _interpreter_frame *_PyEval_GetFrame(void);

PyObject *_Py_MakeCoro(PyFrameConstructor *, struct _interpreter_frame *);
PyObject *_Py_MakeCoro(PyFunctionObject *func, struct _interpreter_frame *);

#ifdef __cplusplus
}
Expand Down
28 changes: 15 additions & 13 deletions Include/internal/pycore_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ enum _framestate {
typedef signed char PyFrameState;

typedef struct _interpreter_frame {
PyObject *f_globals;
PyObject *f_builtins;
PyObject *f_locals;
PyCodeObject *f_code;
PyFrameObject *frame_obj;
/* Borrowed reference to a generator, or NULL */
PyObject *generator;
PyFunctionObject *f_func; /* Strong reference */
PyObject *f_globals; /* Borrowed reference */
PyObject *f_builtins; /* Borrowed reference */
PyObject *f_locals; /* Strong reference, may be NULL */
PyCodeObject *f_code; /* Strong reference */
PyFrameObject *frame_obj; /* Strong reference, may be NULL */
PyObject *generator; /* Borrowed reference, may be NULL */
struct _interpreter_frame *previous;
int f_lasti; /* Last instruction if called */
int stacktop; /* Offset of TOS from localsplus */
Expand Down Expand Up @@ -70,16 +70,18 @@ static inline void _PyFrame_StackPush(InterpreterFrame *f, PyObject *value) {
#define FRAME_SPECIALS_SIZE ((sizeof(InterpreterFrame)-1)/sizeof(PyObject *))

InterpreterFrame *
_PyInterpreterFrame_HeapAlloc(PyFrameConstructor *con, PyObject *locals);
_PyInterpreterFrame_HeapAlloc(PyFunctionObject *func, PyObject *locals);

static inline void
_PyFrame_InitializeSpecials(
InterpreterFrame *frame, PyFrameConstructor *con,
InterpreterFrame *frame, PyFunctionObject *func,
PyObject *locals, int nlocalsplus)
{
frame->f_code = (PyCodeObject *)Py_NewRef(con->fc_code);
frame->f_builtins = Py_NewRef(con->fc_builtins);
frame->f_globals = Py_NewRef(con->fc_globals);
Py_INCREF(func);
frame->f_func = func;
frame->f_code = (PyCodeObject *)Py_NewRef(func->func_code);
frame->f_builtins = func->func_builtins;
frame->f_globals = func->func_globals;
frame->f_locals = Py_XNewRef(locals);
frame->stacktop = nlocalsplus;
frame->frame_obj = NULL;
Expand Down Expand Up @@ -150,7 +152,7 @@ void
_PyFrame_LocalsToFast(InterpreterFrame *frame, int clear);

InterpreterFrame *_PyThreadState_PushFrame(
PyThreadState *tstate, PyFrameConstructor *con, PyObject *locals);
PyThreadState *tstate, PyFunctionObject *func, PyObject *locals);

extern InterpreterFrame *
_PyThreadState_BumpFramePointerSlow(PyThreadState *tstate, size_t size);
Expand Down
11 changes: 11 additions & 0 deletions Include/internal/pycore_function.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef Py_INTERNAL_FUNCTION_H
#define Py_INTERNAL_FUNCTION_H


#include "Python.h"

PyFunctionObject *
_PyFunction_FromConstructor(PyFrameConstructor *constr);


#endif /* !Py_INTERNAL_FUNCTION_H */
1 change: 1 addition & 0 deletions Include/opcode.h

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

3 changes: 2 additions & 1 deletion Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ def _write_atomic(path, data, mode=0o666):
# active exception)
# Python 3.11a3 3464 (bpo-45636: Merge numeric BINARY_*/INPLACE_* into
# BINARY_OP)
# Python 3.11a3 3465 (Add COPY_FREE_VARS opcode)

#
# MAGIC must change whenever the bytecode emitted by the compiler may no
Expand All @@ -379,7 +380,7 @@ def _write_atomic(path, data, mode=0o666):
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated.

MAGIC_NUMBER = (3464).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3465).to_bytes(2, 'little') + b'\r\n'
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

_PYCACHE = '__pycache__'
Expand Down
1 change: 1 addition & 0 deletions Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def jabs_op(name, op):
def_op('MAP_ADD', 147)
def_op('LOAD_CLASSDEREF', 148)
hasfree.append(148)
def_op('COPY_FREE_VARS', 149)

def_op('MATCH_CLASS', 152)

Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@
check_impl_detail, requires_debug_ranges,
gc_collect)
from test.support.script_helper import assert_python_ok
from opcode import opmap
COPY_FREE_VARS = opmap['COPY_FREE_VARS']


def consts(t):
Expand Down Expand Up @@ -185,7 +187,7 @@ def create_closure(__class__):

def new_code(c):
'''A new code object with a __class__ cell added to freevars'''
return c.replace(co_freevars=c.co_freevars + ('__class__',))
return c.replace(co_freevars=c.co_freevars + ('__class__',), co_code=bytes([COPY_FREE_VARS, 1])+c.co_code)

def add_foreign_method(cls, name, f):
code = new_code(f.__code__)
Expand Down
108 changes: 56 additions & 52 deletions Lib/test/test_dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,16 +459,17 @@ def foo(x):

dis_nested_1 = """%s
Disassembly of <code object foo at 0x..., file "%s", line %d>:
0 MAKE_CELL 0 (x)
%3d 2 LOAD_CLOSURE 0 (x)
4 BUILD_TUPLE 1
6 LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
8 MAKE_FUNCTION 8 (closure)
10 LOAD_DEREF 1 (y)
12 GET_ITER
14 CALL_FUNCTION 1
16 RETURN_VALUE
0 COPY_FREE_VARS 1
2 MAKE_CELL 0 (x)
%3d 4 LOAD_CLOSURE 0 (x)
6 BUILD_TUPLE 1
8 LOAD_CONST 1 (<code object <listcomp> at 0x..., file "%s", line %d>)
10 MAKE_FUNCTION 8 (closure)
12 LOAD_DEREF 1 (y)
14 GET_ITER
16 CALL_FUNCTION 1
18 RETURN_VALUE
""" % (dis_nested_0,
__file__,
_h.__code__.co_firstlineno + 1,
Expand All @@ -479,16 +480,18 @@ def foo(x):

dis_nested_2 = """%s
Disassembly of <code object <listcomp> at 0x..., file "%s", line %d>:
%3d 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 6 (to 18)
6 STORE_FAST 1 (z)
8 LOAD_DEREF 2 (x)
10 LOAD_FAST 1 (z)
12 BINARY_OP 0 (+)
14 LIST_APPEND 2
16 JUMP_ABSOLUTE 2 (to 4)
>> 18 RETURN_VALUE
0 COPY_FREE_VARS 1
%3d 2 BUILD_LIST 0
4 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 6 (to 20)
8 STORE_FAST 1 (z)
10 LOAD_DEREF 2 (x)
12 LOAD_FAST 1 (z)
14 BINARY_OP 0 (+)
16 LIST_APPEND 2
18 JUMP_ABSOLUTE 3 (to 6)
>> 20 RETURN_VALUE
""" % (dis_nested_1,
__file__,
_h.__code__.co_firstlineno + 3,
Expand Down Expand Up @@ -1007,42 +1010,43 @@ def _prepare_test_cases():
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=40, starts_line=None, is_jump_target=False, positions=None),
]


expected_opinfo_f = [
Instruction(opname='MAKE_CELL', opcode=135, arg=0, argval='c', argrepr='c', offset=0, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='MAKE_CELL', opcode=135, arg=1, argval='d', argrepr='d', offset=2, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=4, argval=(5, 6), argrepr='(5, 6)', offset=4, starts_line=3, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=3, argval='a', argrepr='a', offset=6, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=4, argval='b', argrepr='b', offset=8, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=0, argval='c', argrepr='c', offset=10, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=1, argval='d', argrepr='d', offset=12, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='BUILD_TUPLE', opcode=102, arg=4, argval=4, argrepr='', offset=14, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=3, argval=code_object_inner, argrepr=repr(code_object_inner), offset=16, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='MAKE_FUNCTION', opcode=132, arg=9, argval=9, argrepr='defaults, closure', offset=18, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='STORE_FAST', opcode=125, arg=2, argval='inner', argrepr='inner', offset=20, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=0, argval='print', argrepr='print', offset=22, starts_line=5, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=3, argval='a', argrepr='a', offset=24, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=4, argval='b', argrepr='b', offset=26, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=0, argval='c', argrepr='c', offset=28, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=1, argval='d', argrepr='d', offset=30, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='CALL_FUNCTION', opcode=131, arg=4, argval=4, argrepr='', offset=32, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=34, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=2, argval='inner', argrepr='inner', offset=36, starts_line=6, is_jump_target=False, positions=None),
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=38, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='COPY_FREE_VARS', opcode=149, arg=2, argval=2, argrepr='', offset=0, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='MAKE_CELL', opcode=135, arg=0, argval='c', argrepr='c', offset=2, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='MAKE_CELL', opcode=135, arg=1, argval='d', argrepr='d', offset=4, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=4, argval=(5, 6), argrepr='(5, 6)', offset=6, starts_line=3, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=3, argval='a', argrepr='a', offset=8, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=4, argval='b', argrepr='b', offset=10, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=0, argval='c', argrepr='c', offset=12, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CLOSURE', opcode=136, arg=1, argval='d', argrepr='d', offset=14, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='BUILD_TUPLE', opcode=102, arg=4, argval=4, argrepr='', offset=16, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=3, argval=code_object_inner, argrepr=repr(code_object_inner), offset=18, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='MAKE_FUNCTION', opcode=132, arg=9, argval=9, argrepr='defaults, closure', offset=20, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='STORE_FAST', opcode=125, arg=2, argval='inner', argrepr='inner', offset=22, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=0, argval='print', argrepr='print', offset=24, starts_line=5, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=3, argval='a', argrepr='a', offset=26, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=4, argval='b', argrepr='b', offset=28, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=0, argval='c', argrepr='c', offset=30, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=1, argval='d', argrepr='d', offset=32, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='CALL_FUNCTION', opcode=131, arg=4, argval=4, argrepr='', offset=34, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=36, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=2, argval='inner', argrepr='inner', offset=38, starts_line=6, is_jump_target=False, positions=None),
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=40, starts_line=None, is_jump_target=False, positions=None),
]

expected_opinfo_inner = [
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=0, argval='print', argrepr='print', offset=0, starts_line=4, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=2, argval='a', argrepr='a', offset=2, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=3, argval='b', argrepr='b', offset=4, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=4, argval='c', argrepr='c', offset=6, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=5, argval='d', argrepr='d', offset=8, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='e', argrepr='e', offset=10, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=1, argval='f', argrepr='f', offset=12, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='CALL_FUNCTION', opcode=131, arg=6, argval=6, argrepr='', offset=14, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=16, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=0, argval=None, argrepr='None', offset=18, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=20, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='COPY_FREE_VARS', opcode=149, arg=4, argval=4, argrepr='', offset=0, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_GLOBAL', opcode=116, arg=0, argval='print', argrepr='print', offset=2, starts_line=4, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=2, argval='a', argrepr='a', offset=4, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=3, argval='b', argrepr='b', offset=6, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=4, argval='c', argrepr='c', offset=8, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_DEREF', opcode=137, arg=5, argval='d', argrepr='d', offset=10, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='e', argrepr='e', offset=12, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_FAST', opcode=124, arg=1, argval='f', argrepr='f', offset=14, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='CALL_FUNCTION', opcode=131, arg=6, argval=6, argrepr='', offset=16, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='POP_TOP', opcode=1, arg=None, argval=None, argrepr='', offset=18, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='LOAD_CONST', opcode=100, arg=0, argval=None, argrepr='None', offset=20, starts_line=None, is_jump_target=False, positions=None),
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=22, starts_line=None, is_jump_target=False, positions=None),
]

expected_opinfo_jumpy = [
Expand Down
1 change: 1 addition & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/internal/pycore_fileutils.h \
$(srcdir)/Include/internal/pycore_floatobject.h \
$(srcdir)/Include/internal/pycore_format.h \
$(srcdir)/Include/internal/pycore_function.h \
$(srcdir)/Include/internal/pycore_getopt.h \
$(srcdir)/Include/internal/pycore_gil.h \
$(srcdir)/Include/internal/pycore_hamt.h \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Adds new :opcode:`COPY_FREE_VARS` opcode, to make copying of free variables
from function to frame explicit. Helps optimization of calls to Python
function.
6 changes: 3 additions & 3 deletions Objects/call.c
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,16 @@ _PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
size_t nargsf, PyObject *kwnames)
{
assert(PyFunction_Check(func));
PyFrameConstructor *f = PyFunction_AS_FRAME_CONSTRUCTOR(func);
PyFunctionObject *f = (PyFunctionObject *)func;
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
assert(nargs >= 0);
PyThreadState *tstate = _PyThreadState_GET();
assert(nargs == 0 || stack != NULL);
if (((PyCodeObject *)f->fc_code)->co_flags & CO_OPTIMIZED) {
if (((PyCodeObject *)f->func_code)->co_flags & CO_OPTIMIZED) {
return _PyEval_Vector(tstate, f, NULL, stack, nargs, kwnames);
}
else {
return _PyEval_Vector(tstate, f, f->fc_globals, stack, nargs, kwnames);
return _PyEval_Vector(tstate, f, f->func_globals, stack, nargs, kwnames);
}
}

Expand Down
Loading

0 comments on commit 135cabd

Please sign in to comment.