Skip to content

bpo-26110: Add CALL_METHOD_KW opcode to speedup method calls with keywords #26014

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 18 commits into from
May 15, 2021
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
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 @@ -353,6 +353,7 @@ def _write_atomic(path, data, mode=0o666):
# Python 3.10b1 3438 Safer line number table handling.
# Python 3.10b1 3439 (Add ROT_N)
# Python 3.11a1 3450 Use exception table for unwinding ("zero cost" exception handling)
# Python 3.11a1 3451 (Add CALL_METHOD_KW)

#
# MAGIC must change whenever the bytecode emitted by the compiler may no
Expand All @@ -362,7 +363,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 = (3450).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3451).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 @@ -213,5 +213,6 @@ def jabs_op(name, op):
def_op('SET_UPDATE', 163)
def_op('DICT_MERGE', 164)
def_op('DICT_UPDATE', 165)
def_op('CALL_METHOD_KW', 166)

del def_op, name_op, jrel_op, jabs_op
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add ``CALL_METHOD_KW`` opcode to speed up method calls with keyword
arguments. Idea originated from PyPy. A side effect is executing
``CALL_METHOD`` is now branchless in the evaluation loop.
68 changes: 42 additions & 26 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1421,7 +1421,7 @@ eval_frame_handle_pending(PyThreadState *tstate)
#define STACK_SHRINK(n) do { \
assert(n >= 0); \
(void)(lltrace && prtrace(tstate, TOP(), "stackadj")); \
(void)(BASIC_STACKADJ(-n)); \
(void)(BASIC_STACKADJ(-(n))); \
assert(STACK_LEVEL() <= co->co_stacksize); \
} while (0)
#define EXT_POP(STACK_POINTER) ((void)(lltrace && \
Expand All @@ -1431,7 +1431,7 @@ eval_frame_handle_pending(PyThreadState *tstate)
#define PUSH(v) BASIC_PUSH(v)
#define POP() BASIC_POP()
#define STACK_GROW(n) BASIC_STACKADJ(n)
#define STACK_SHRINK(n) BASIC_STACKADJ(-n)
#define STACK_SHRINK(n) BASIC_STACKADJ(-(n))
#define EXT_POP(STACK_POINTER) (*--(STACK_POINTER))
#endif

Expand Down Expand Up @@ -4164,54 +4164,70 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)

case TARGET(CALL_METHOD): {
/* Designed to work in tamdem with LOAD_METHOD. */
PyObject **sp, *res, *meth;
PyObject **sp, *res;
int meth_found;

sp = stack_pointer;
/* `meth` is NULL when LOAD_METHOD thinks that it's not
a method call.

meth = PEEK(oparg + 2);
if (meth == NULL) {
/* `meth` is NULL when LOAD_METHOD thinks that it's not
a method call.

Stack layout:
Stack layout:

... | NULL | callable | arg1 | ... | argN
^- TOP()
^- (-oparg)
^- (-oparg-1)
^- (-oparg-2)

`callable` will be POPed by call_function.
NULL will will be POPed manually later.
*/
res = call_function(tstate, &trace_info, &sp, oparg, NULL);
stack_pointer = sp;
(void)POP(); /* POP the NULL. */
}
else {
/* This is a method call. Stack layout:
`callable` will be POPed by call_function.
NULL will will be POPed manually later.
If `meth` isn't NULL, it's a method call. Stack layout:

... | method | self | arg1 | ... | argN
^- TOP()
^- (-oparg)
^- (-oparg-1)
^- (-oparg-2)

`self` and `method` will be POPed by call_function.
We'll be passing `oparg + 1` to call_function, to
make it accept the `self` as a first argument.
*/
res = call_function(tstate, &trace_info, &sp, oparg + 1, NULL);
stack_pointer = sp;
}
`self` and `method` will be POPed by call_function.
We'll be passing `oparg + 1` to call_function, to
make it accept the `self` as a first argument.
*/
meth_found = (PEEK(oparg + 2) != NULL);
res = call_function(tstate, &trace_info, &sp, oparg + meth_found, NULL);
stack_pointer = sp;

STACK_SHRINK(1 - meth_found);
PUSH(res);
if (res == NULL)
if (res == NULL) {
goto error;
}
CHECK_EVAL_BREAKER();
DISPATCH();
}
case TARGET(CALL_METHOD_KW): {
/* Designed to work in tandem with LOAD_METHOD. Same as CALL_METHOD
but pops TOS to get a tuple of keyword names. */
PyObject **sp, *res;
PyObject *names = NULL;
int meth_found;

names = POP();

sp = stack_pointer;
meth_found = (PEEK(oparg + 2) != NULL);
res = call_function(tstate, &trace_info, &sp, oparg + meth_found, names);
stack_pointer = sp;

STACK_SHRINK(1 - meth_found);
PUSH(res);
Py_DECREF(names);
if (res == NULL) {
goto error;
}
CHECK_EVAL_BREAKER();
DISPATCH();
}
case TARGET(CALL_FUNCTION): {
PREDICTED(CALL_FUNCTION);
PyObject **sp, *res;
Expand Down
81 changes: 63 additions & 18 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ static int are_all_items_const(asdl_expr_seq *, Py_ssize_t, Py_ssize_t);
static int compiler_with(struct compiler *, stmt_ty, int);
static int compiler_async_with(struct compiler *, stmt_ty, int);
static int compiler_async_for(struct compiler *, stmt_ty);
static int validate_keywords(struct compiler *c, asdl_keyword_seq *keywords);
static int compiler_call_simple_kw_helper(struct compiler *c,
asdl_keyword_seq *keywords,
Py_ssize_t nkwelts);
static int compiler_call_helper(struct compiler *c, int n,
asdl_expr_seq *args,
asdl_keyword_seq *keywords);
Expand Down Expand Up @@ -1176,6 +1180,8 @@ stack_effect(int opcode, int oparg, int jump)
return -oparg;
case CALL_METHOD:
return -oparg-1;
case CALL_METHOD_KW:
return -oparg-2;
case CALL_FUNCTION_KW:
return -oparg-1;
case CALL_FUNCTION_EX:
Expand Down Expand Up @@ -4266,19 +4272,19 @@ check_index(struct compiler *c, expr_ty e, expr_ty s)
static int
maybe_optimize_method_call(struct compiler *c, expr_ty e)
{
Py_ssize_t argsl, i;
Py_ssize_t argsl, i, kwdsl;
expr_ty meth = e->v.Call.func;
asdl_expr_seq *args = e->v.Call.args;
asdl_keyword_seq *kwds = e->v.Call.keywords;

/* Check that the call node is an attribute access, and that
the call doesn't have keyword parameters. */
if (meth->kind != Attribute_kind || meth->v.Attribute.ctx != Load ||
asdl_seq_LEN(e->v.Call.keywords)) {
/* Check that the call node is an attribute access */
if (meth->kind != Attribute_kind || meth->v.Attribute.ctx != Load) {
return -1;
}
/* Check that there aren't too many arguments */
argsl = asdl_seq_LEN(args);
if (argsl >= STACK_USE_GUIDELINE) {
kwdsl = asdl_seq_LEN(kwds);
if (argsl + kwdsl + (kwdsl != 0) >= STACK_USE_GUIDELINE) {
return -1;
}
/* Check that there are no *varargs types of arguments. */
Expand All @@ -4289,13 +4295,28 @@ maybe_optimize_method_call(struct compiler *c, expr_ty e)
}
}

for (i = 0; i < kwdsl; i++) {
keyword_ty kw = asdl_seq_GET(kwds, i);
if (kw->arg == NULL) {
return -1;
}
}
/* Alright, we can optimize the code. */
VISIT(c, expr, meth->v.Attribute.value);
int old_lineno = c->u->u_lineno;
c->u->u_lineno = meth->end_lineno;
ADDOP_NAME(c, LOAD_METHOD, meth->v.Attribute.attr, names);
VISIT_SEQ(c, expr, e->v.Call.args);
ADDOP_I(c, CALL_METHOD, asdl_seq_LEN(e->v.Call.args));

if (kwdsl) {
if (!compiler_call_simple_kw_helper(c, kwds, kwdsl)) {
return 0;
};
ADDOP_I(c, CALL_METHOD_KW, argsl + kwdsl);
}
else {
ADDOP_I(c, CALL_METHOD, argsl);
}
c->u->u_lineno = old_lineno;
return 1;
}
Expand Down Expand Up @@ -4327,6 +4348,9 @@ validate_keywords(struct compiler *c, asdl_keyword_seq *keywords)
static int
compiler_call(struct compiler *c, expr_ty e)
{
if (validate_keywords(c, e->v.Call.keywords) == -1) {
return 0;
}
int ret = maybe_optimize_method_call(c, e);
if (ret >= 0) {
return ret;
Expand Down Expand Up @@ -4458,6 +4482,36 @@ compiler_subkwargs(struct compiler *c, asdl_keyword_seq *keywords, Py_ssize_t be
return 1;
}

/* Used by compiler_call_helper and maybe_optimize_method_call to emit
LOAD_CONST kw1
LOAD_CONST kw2
...
LOAD_CONST <tuple of kwnames>
before a CALL_(FUNCTION|METHOD)_KW.

Returns 1 on success, 0 on error.
*/
static int
compiler_call_simple_kw_helper(struct compiler *c,
asdl_keyword_seq *keywords,
Py_ssize_t nkwelts)
{
PyObject *names;
VISIT_SEQ(c, keyword, keywords);
names = PyTuple_New(nkwelts);
if (names == NULL) {
return 0;
}
for (int i = 0; i < nkwelts; i++) {
keyword_ty kw = asdl_seq_GET(keywords, i);
Py_INCREF(kw->arg);
PyTuple_SET_ITEM(names, i, kw->arg);
}
ADDOP_LOAD_CONST_NEW(c, names);
return 1;
}


/* shared code between compiler_call and compiler_class */
static int
compiler_call_helper(struct compiler *c,
Expand Down Expand Up @@ -4497,18 +4551,9 @@ compiler_call_helper(struct compiler *c,
VISIT(c, expr, elt);
}
if (nkwelts) {
PyObject *names;
VISIT_SEQ(c, keyword, keywords);
names = PyTuple_New(nkwelts);
if (names == NULL) {
if (!compiler_call_simple_kw_helper(c, keywords, nkwelts)) {
return 0;
}
for (i = 0; i < nkwelts; i++) {
keyword_ty kw = asdl_seq_GET(keywords, i);
Py_INCREF(kw->arg);
PyTuple_SET_ITEM(names, i, kw->arg);
}
ADDOP_LOAD_CONST_NEW(c, names);
};
ADDOP_I(c, CALL_FUNCTION_KW, n + nelts + nkwelts);
return 1;
}
Expand Down
10 changes: 5 additions & 5 deletions Python/importlib.h

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

Loading