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
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
@@ -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
@@ -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__'
1 change: 1 addition & 0 deletions Lib/opcode.py
Original file line number Diff line number Diff line change
@@ -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
@@ -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 && \
@@ -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

@@ -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;
81 changes: 63 additions & 18 deletions Python/compile.c
Original file line number Diff line number Diff line change
@@ -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);
@@ -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:
@@ -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. */
@@ -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;
}
@@ -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;
@@ -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,
@@ -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;
}
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.

246 changes: 123 additions & 123 deletions Python/importlib_external.h

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Python/importlib_zipimport.h
Original file line number Diff line number Diff line change
@@ -282,13 +282,13 @@ const unsigned char _Py_M__zipimport[] = {
254,16,3,114,11,0,0,0,122,23,122,105,112,105,109,112,
111,114,116,101,114,46,102,105,110,100,95,109,111,100,117,108,
101,99,3,0,0,0,0,0,0,0,0,0,0,0,7,0,
0,0,5,0,0,0,67,0,0,0,115,108,0,0,0,116,
0,0,6,0,0,0,67,0,0,0,115,108,0,0,0,116,
0,124,0,124,1,131,2,125,3,124,3,100,1,117,1,114,
17,116,1,106,2,124,1,124,0,124,3,100,2,141,3,83,
17,116,1,160,2,124,1,124,0,124,3,100,2,166,3,83,
0,116,3,124,0,124,1,131,2,125,4,116,4,124,0,124,
4,131,2,114,52,124,0,106,5,155,0,116,6,155,0,124,
4,155,0,157,3,125,5,116,1,106,7,124,1,100,1,100,
3,100,4,141,3,125,6,124,6,106,8,160,9,124,5,161,
4,155,0,157,3,125,5,116,1,160,7,124,1,100,1,100,
3,100,4,166,3,125,6,124,6,106,8,160,9,124,5,161,
1,1,0,124,6,83,0,100,1,83,0,41,5,122,107,67,
114,101,97,116,101,32,97,32,77,111,100,117,108,101,83,112,
101,99,32,102,111,114,32,116,104,101,32,115,112,101,99,105,
@@ -1047,8 +1047,8 @@ const unsigned char _Py_M__zipimport[] = {
0,0,0,67,0,0,0,115,14,1,0,0,116,0,124,0,
124,1,131,2,125,2,100,0,125,3,116,1,68,0,93,100,
92,3,125,4,125,5,125,6,124,2,124,4,23,0,125,7,
116,2,106,3,100,1,124,0,106,4,116,5,124,7,100,2,
100,3,141,5,1,0,9,0,124,0,106,6,124,7,25,0,
116,2,160,3,100,1,124,0,106,4,116,5,124,7,100,2,
100,3,166,5,1,0,9,0,124,0,106,6,124,7,25,0,
125,8,110,10,35,0,4,0,116,7,121,134,1,0,1,0,
1,0,89,0,113,9,37,0,124,8,100,4,25,0,125,9,
116,8,124,0,106,4,124,8,131,2,125,10,100,0,125,11,
2 changes: 1 addition & 1 deletion Python/opcode_targets.h

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