Skip to content

Commit f24afda

Browse files
bpo-26110: Add CALL_METHOD_KW opcode to speedup method calls with keywords (GH-26014)
* Add CALL_METHOD_KW * Make CALL_METHOD branchless too since it shares the same code * Place parentheses in STACK_SHRINK
1 parent e4e931a commit f24afda

File tree

10 files changed

+247
-180
lines changed

10 files changed

+247
-180
lines changed

Include/opcode.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ def _write_atomic(path, data, mode=0o666):
353353
# Python 3.10b1 3438 Safer line number table handling.
354354
# Python 3.10b1 3439 (Add ROT_N)
355355
# Python 3.11a1 3450 Use exception table for unwinding ("zero cost" exception handling)
356+
# Python 3.11a1 3451 (Add CALL_METHOD_KW)
356357

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

365-
MAGIC_NUMBER = (3450).to_bytes(2, 'little') + b'\r\n'
366+
MAGIC_NUMBER = (3451).to_bytes(2, 'little') + b'\r\n'
366367
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
367368

368369
_PYCACHE = '__pycache__'

Lib/opcode.py

+1
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,6 @@ def jabs_op(name, op):
213213
def_op('SET_UPDATE', 163)
214214
def_op('DICT_MERGE', 164)
215215
def_op('DICT_UPDATE', 165)
216+
def_op('CALL_METHOD_KW', 166)
216217

217218
del def_op, name_op, jrel_op, jabs_op
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add ``CALL_METHOD_KW`` opcode to speed up method calls with keyword
2+
arguments. Idea originated from PyPy. A side effect is executing
3+
``CALL_METHOD`` is now branchless in the evaluation loop.

Python/ceval.c

+42-26
Original file line numberDiff line numberDiff line change
@@ -1421,7 +1421,7 @@ eval_frame_handle_pending(PyThreadState *tstate)
14211421
#define STACK_SHRINK(n) do { \
14221422
assert(n >= 0); \
14231423
(void)(lltrace && prtrace(tstate, TOP(), "stackadj")); \
1424-
(void)(BASIC_STACKADJ(-n)); \
1424+
(void)(BASIC_STACKADJ(-(n))); \
14251425
assert(STACK_LEVEL() <= co->co_stacksize); \
14261426
} while (0)
14271427
#define EXT_POP(STACK_POINTER) ((void)(lltrace && \
@@ -1431,7 +1431,7 @@ eval_frame_handle_pending(PyThreadState *tstate)
14311431
#define PUSH(v) BASIC_PUSH(v)
14321432
#define POP() BASIC_POP()
14331433
#define STACK_GROW(n) BASIC_STACKADJ(n)
1434-
#define STACK_SHRINK(n) BASIC_STACKADJ(-n)
1434+
#define STACK_SHRINK(n) BASIC_STACKADJ(-(n))
14351435
#define EXT_POP(STACK_POINTER) (*--(STACK_POINTER))
14361436
#endif
14371437

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

41654165
case TARGET(CALL_METHOD): {
41664166
/* Designed to work in tamdem with LOAD_METHOD. */
4167-
PyObject **sp, *res, *meth;
4167+
PyObject **sp, *res;
4168+
int meth_found;
41684169

41694170
sp = stack_pointer;
4171+
/* `meth` is NULL when LOAD_METHOD thinks that it's not
4172+
a method call.
41704173
4171-
meth = PEEK(oparg + 2);
4172-
if (meth == NULL) {
4173-
/* `meth` is NULL when LOAD_METHOD thinks that it's not
4174-
a method call.
4175-
4176-
Stack layout:
4174+
Stack layout:
41774175
41784176
... | NULL | callable | arg1 | ... | argN
41794177
^- TOP()
41804178
^- (-oparg)
41814179
^- (-oparg-1)
41824180
^- (-oparg-2)
41834181
4184-
`callable` will be POPed by call_function.
4185-
NULL will will be POPed manually later.
4186-
*/
4187-
res = call_function(tstate, &trace_info, &sp, oparg, NULL);
4188-
stack_pointer = sp;
4189-
(void)POP(); /* POP the NULL. */
4190-
}
4191-
else {
4192-
/* This is a method call. Stack layout:
4182+
`callable` will be POPed by call_function.
4183+
NULL will will be POPed manually later.
4184+
If `meth` isn't NULL, it's a method call. Stack layout:
41934185
41944186
... | method | self | arg1 | ... | argN
41954187
^- TOP()
41964188
^- (-oparg)
41974189
^- (-oparg-1)
41984190
^- (-oparg-2)
41994191
4200-
`self` and `method` will be POPed by call_function.
4201-
We'll be passing `oparg + 1` to call_function, to
4202-
make it accept the `self` as a first argument.
4203-
*/
4204-
res = call_function(tstate, &trace_info, &sp, oparg + 1, NULL);
4205-
stack_pointer = sp;
4206-
}
4192+
`self` and `method` will be POPed by call_function.
4193+
We'll be passing `oparg + 1` to call_function, to
4194+
make it accept the `self` as a first argument.
4195+
*/
4196+
meth_found = (PEEK(oparg + 2) != NULL);
4197+
res = call_function(tstate, &trace_info, &sp, oparg + meth_found, NULL);
4198+
stack_pointer = sp;
42074199

4200+
STACK_SHRINK(1 - meth_found);
42084201
PUSH(res);
4209-
if (res == NULL)
4202+
if (res == NULL) {
42104203
goto error;
4204+
}
42114205
CHECK_EVAL_BREAKER();
42124206
DISPATCH();
42134207
}
4208+
case TARGET(CALL_METHOD_KW): {
4209+
/* Designed to work in tandem with LOAD_METHOD. Same as CALL_METHOD
4210+
but pops TOS to get a tuple of keyword names. */
4211+
PyObject **sp, *res;
4212+
PyObject *names = NULL;
4213+
int meth_found;
42144214

4215+
names = POP();
4216+
4217+
sp = stack_pointer;
4218+
meth_found = (PEEK(oparg + 2) != NULL);
4219+
res = call_function(tstate, &trace_info, &sp, oparg + meth_found, names);
4220+
stack_pointer = sp;
4221+
4222+
STACK_SHRINK(1 - meth_found);
4223+
PUSH(res);
4224+
Py_DECREF(names);
4225+
if (res == NULL) {
4226+
goto error;
4227+
}
4228+
CHECK_EVAL_BREAKER();
4229+
DISPATCH();
4230+
}
42154231
case TARGET(CALL_FUNCTION): {
42164232
PREDICTED(CALL_FUNCTION);
42174233
PyObject **sp, *res;

Python/compile.c

+63-18
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,10 @@ static int are_all_items_const(asdl_expr_seq *, Py_ssize_t, Py_ssize_t);
309309
static int compiler_with(struct compiler *, stmt_ty, int);
310310
static int compiler_async_with(struct compiler *, stmt_ty, int);
311311
static int compiler_async_for(struct compiler *, stmt_ty);
312+
static int validate_keywords(struct compiler *c, asdl_keyword_seq *keywords);
313+
static int compiler_call_simple_kw_helper(struct compiler *c,
314+
asdl_keyword_seq *keywords,
315+
Py_ssize_t nkwelts);
312316
static int compiler_call_helper(struct compiler *c, int n,
313317
asdl_expr_seq *args,
314318
asdl_keyword_seq *keywords);
@@ -1176,6 +1180,8 @@ stack_effect(int opcode, int oparg, int jump)
11761180
return -oparg;
11771181
case CALL_METHOD:
11781182
return -oparg-1;
1183+
case CALL_METHOD_KW:
1184+
return -oparg-2;
11791185
case CALL_FUNCTION_KW:
11801186
return -oparg-1;
11811187
case CALL_FUNCTION_EX:
@@ -4266,19 +4272,19 @@ check_index(struct compiler *c, expr_ty e, expr_ty s)
42664272
static int
42674273
maybe_optimize_method_call(struct compiler *c, expr_ty e)
42684274
{
4269-
Py_ssize_t argsl, i;
4275+
Py_ssize_t argsl, i, kwdsl;
42704276
expr_ty meth = e->v.Call.func;
42714277
asdl_expr_seq *args = e->v.Call.args;
4278+
asdl_keyword_seq *kwds = e->v.Call.keywords;
42724279

4273-
/* Check that the call node is an attribute access, and that
4274-
the call doesn't have keyword parameters. */
4275-
if (meth->kind != Attribute_kind || meth->v.Attribute.ctx != Load ||
4276-
asdl_seq_LEN(e->v.Call.keywords)) {
4280+
/* Check that the call node is an attribute access */
4281+
if (meth->kind != Attribute_kind || meth->v.Attribute.ctx != Load) {
42774282
return -1;
42784283
}
42794284
/* Check that there aren't too many arguments */
42804285
argsl = asdl_seq_LEN(args);
4281-
if (argsl >= STACK_USE_GUIDELINE) {
4286+
kwdsl = asdl_seq_LEN(kwds);
4287+
if (argsl + kwdsl + (kwdsl != 0) >= STACK_USE_GUIDELINE) {
42824288
return -1;
42834289
}
42844290
/* Check that there are no *varargs types of arguments. */
@@ -4289,13 +4295,28 @@ maybe_optimize_method_call(struct compiler *c, expr_ty e)
42894295
}
42904296
}
42914297

4298+
for (i = 0; i < kwdsl; i++) {
4299+
keyword_ty kw = asdl_seq_GET(kwds, i);
4300+
if (kw->arg == NULL) {
4301+
return -1;
4302+
}
4303+
}
42924304
/* Alright, we can optimize the code. */
42934305
VISIT(c, expr, meth->v.Attribute.value);
42944306
int old_lineno = c->u->u_lineno;
42954307
c->u->u_lineno = meth->end_lineno;
42964308
ADDOP_NAME(c, LOAD_METHOD, meth->v.Attribute.attr, names);
42974309
VISIT_SEQ(c, expr, e->v.Call.args);
4298-
ADDOP_I(c, CALL_METHOD, asdl_seq_LEN(e->v.Call.args));
4310+
4311+
if (kwdsl) {
4312+
if (!compiler_call_simple_kw_helper(c, kwds, kwdsl)) {
4313+
return 0;
4314+
};
4315+
ADDOP_I(c, CALL_METHOD_KW, argsl + kwdsl);
4316+
}
4317+
else {
4318+
ADDOP_I(c, CALL_METHOD, argsl);
4319+
}
42994320
c->u->u_lineno = old_lineno;
43004321
return 1;
43014322
}
@@ -4327,6 +4348,9 @@ validate_keywords(struct compiler *c, asdl_keyword_seq *keywords)
43274348
static int
43284349
compiler_call(struct compiler *c, expr_ty e)
43294350
{
4351+
if (validate_keywords(c, e->v.Call.keywords) == -1) {
4352+
return 0;
4353+
}
43304354
int ret = maybe_optimize_method_call(c, e);
43314355
if (ret >= 0) {
43324356
return ret;
@@ -4458,6 +4482,36 @@ compiler_subkwargs(struct compiler *c, asdl_keyword_seq *keywords, Py_ssize_t be
44584482
return 1;
44594483
}
44604484

4485+
/* Used by compiler_call_helper and maybe_optimize_method_call to emit
4486+
LOAD_CONST kw1
4487+
LOAD_CONST kw2
4488+
...
4489+
LOAD_CONST <tuple of kwnames>
4490+
before a CALL_(FUNCTION|METHOD)_KW.
4491+
4492+
Returns 1 on success, 0 on error.
4493+
*/
4494+
static int
4495+
compiler_call_simple_kw_helper(struct compiler *c,
4496+
asdl_keyword_seq *keywords,
4497+
Py_ssize_t nkwelts)
4498+
{
4499+
PyObject *names;
4500+
VISIT_SEQ(c, keyword, keywords);
4501+
names = PyTuple_New(nkwelts);
4502+
if (names == NULL) {
4503+
return 0;
4504+
}
4505+
for (int i = 0; i < nkwelts; i++) {
4506+
keyword_ty kw = asdl_seq_GET(keywords, i);
4507+
Py_INCREF(kw->arg);
4508+
PyTuple_SET_ITEM(names, i, kw->arg);
4509+
}
4510+
ADDOP_LOAD_CONST_NEW(c, names);
4511+
return 1;
4512+
}
4513+
4514+
44614515
/* shared code between compiler_call and compiler_class */
44624516
static int
44634517
compiler_call_helper(struct compiler *c,
@@ -4497,18 +4551,9 @@ compiler_call_helper(struct compiler *c,
44974551
VISIT(c, expr, elt);
44984552
}
44994553
if (nkwelts) {
4500-
PyObject *names;
4501-
VISIT_SEQ(c, keyword, keywords);
4502-
names = PyTuple_New(nkwelts);
4503-
if (names == NULL) {
4554+
if (!compiler_call_simple_kw_helper(c, keywords, nkwelts)) {
45044555
return 0;
4505-
}
4506-
for (i = 0; i < nkwelts; i++) {
4507-
keyword_ty kw = asdl_seq_GET(keywords, i);
4508-
Py_INCREF(kw->arg);
4509-
PyTuple_SET_ITEM(names, i, kw->arg);
4510-
}
4511-
ADDOP_LOAD_CONST_NEW(c, names);
4556+
};
45124557
ADDOP_I(c, CALL_FUNCTION_KW, n + nelts + nkwelts);
45134558
return 1;
45144559
}

Python/importlib.h

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)