Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit d7ac01f

Browse files
author
Anselm Kruis
committed
Merge branch master into master-slp.
Issue python#26110: Add LOAD_METHOD/CALL_METHOD opcodes. The outcome of this merge is not functional.
2 parents 85f17fa + f239213 commit d7ac01f

File tree

13 files changed

+747
-483
lines changed

13 files changed

+747
-483
lines changed

Doc/whatsnew/3.7.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ Improved Modules
9292
Optimizations
9393
=============
9494

95+
* Added two new opcodes: ``LOAD_METHOD`` and ``CALL_METHOD`` to avoid
96+
instantiation of bound method objects for method calls, which results
97+
in method calls being faster up to 20%.
98+
(Contributed by Yury Selivanov and INADA Naoki in :issue:`26110`.)
99+
95100

96101
Build and C API Changes
97102
=======================

Include/opcode.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ extern "C" {
126126
#define BUILD_CONST_KEY_MAP 156
127127
#define BUILD_STRING 157
128128
#define BUILD_TUPLE_UNPACK_WITH_CALL 158
129+
#define LOAD_METHOD 160
130+
#define CALL_METHOD 161
129131

130132
/* EXCEPT_HANDLER is a special, implicit block type which is created when
131133
entering an except handler. It is not an opcode but we define it here

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def _write_atomic(path, data, mode=0o666):
240240
# Python 3.6b1 3377 (set __class__ cell from type.__new__ #23722)
241241
# Python 3.6b2 3378 (add BUILD_TUPLE_UNPACK_WITH_CALL #28257)
242242
# Python 3.6rc1 3379 (more thorough __class__ validation #23722)
243+
# Python 3.7a0 3390 (add LOAD_METHOD and CALL_METHOD opcodes)
243244
#
244245
# MAGIC must change whenever the bytecode emitted by the compiler may no
245246
# longer be understood by older implementations of the eval loop (usually
@@ -248,7 +249,7 @@ def _write_atomic(path, data, mode=0o666):
248249
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
249250
# in PC/launcher.c must also be updated.
250251

251-
MAGIC_NUMBER = (3379).to_bytes(2, 'little') + b'\r\n'
252+
MAGIC_NUMBER = (3390).to_bytes(2, 'little') + b'\r\n'
252253
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
253254

254255
_PYCACHE = '__pycache__'

Lib/opcode.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,7 @@ def jabs_op(name, op):
212212
def_op('BUILD_STRING', 157)
213213
def_op('BUILD_TUPLE_UNPACK_WITH_CALL', 158)
214214

215+
name_op('LOAD_METHOD', 160)
216+
def_op('CALL_METHOD', 161)
217+
215218
del def_op, name_op, jrel_op, jabs_op

Lib/test/test_syntax.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,33 @@
207207
... # doctest: +ELLIPSIS
208208
() [('a000', 0), ('a001', 1), ('a002', 2), ..., ('a298', 298), ('a299', 299)]
209209
210+
>>> class C:
211+
... def meth(self, *args):
212+
... return args
213+
>>> obj = C()
214+
>>> obj.meth(
215+
... 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
216+
... 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
217+
... 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
218+
... 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
219+
... 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
220+
... 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
221+
... 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121,
222+
... 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135,
223+
... 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149,
224+
... 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163,
225+
... 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177,
226+
... 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191,
227+
... 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205,
228+
... 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
229+
... 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
230+
... 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247,
231+
... 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
232+
... 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275,
233+
... 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289,
234+
... 290, 291, 292, 293, 294, 295, 296, 297, 298, 299) # doctest: +ELLIPSIS
235+
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 297, 298, 299)
236+
210237
>>> f(lambda x: x[0] = 3)
211238
Traceback (most recent call last):
212239
SyntaxError: lambda cannot contain assignment

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ Core and Builtins
174174
- Issue #28721: Fix asynchronous generators aclose() and athrow() to
175175
handle StopAsyncIteration propagation properly.
176176

177+
- Issue #26110: Speed-up method calls: add LOAD_METHOD and CALL_METHOD
178+
opcodes.
179+
177180
Library
178181
-------
179182

Objects/object.c

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1028,11 +1028,99 @@ _PyObject_NextNotImplemented(PyObject *self)
10281028
return NULL;
10291029
}
10301030

1031-
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot */
1031+
1032+
/* Specialized version of _PyObject_GenericGetAttrWithDict
1033+
specifically for the LOAD_METHOD opcode.
1034+
1035+
Return 1 if a method is found, 0 if it's a regular attribute
1036+
from __dict__ or something returned by using a descriptor
1037+
protocol.
1038+
1039+
`method` will point to the resolved attribute or NULL. In the
1040+
latter case, an error will be set.
1041+
*/
1042+
int
1043+
_PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
1044+
{
1045+
PyTypeObject *tp = Py_TYPE(obj);
1046+
PyObject *descr;
1047+
descrgetfunc f = NULL;
1048+
PyObject **dictptr, *dict;
1049+
PyObject *attr;
1050+
int meth_found = 0;
1051+
1052+
assert(*method == NULL);
1053+
1054+
if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr
1055+
|| !PyUnicode_Check(name)) {
1056+
*method = PyObject_GetAttr(obj, name);
1057+
return 0;
1058+
}
1059+
1060+
if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
1061+
return 0;
1062+
1063+
descr = _PyType_Lookup(tp, name);
1064+
if (descr != NULL) {
1065+
Py_INCREF(descr);
1066+
if (PyFunction_Check(descr)) {
1067+
/* A python method. */
1068+
meth_found = 1;
1069+
} else {
1070+
f = descr->ob_type->tp_descr_get;
1071+
if (f != NULL && PyDescr_IsData(descr)) {
1072+
*method = f(descr, obj, (PyObject *)obj->ob_type);
1073+
Py_DECREF(descr);
1074+
return 0;
1075+
}
1076+
}
1077+
}
1078+
1079+
dictptr = _PyObject_GetDictPtr(obj);
1080+
if (dictptr != NULL && (dict = *dictptr) != NULL) {
1081+
Py_INCREF(dict);
1082+
attr = PyDict_GetItem(dict, name);
1083+
if (attr != NULL) {
1084+
Py_INCREF(attr);
1085+
*method = attr;
1086+
Py_DECREF(dict);
1087+
Py_XDECREF(descr);
1088+
return 0;
1089+
}
1090+
Py_DECREF(dict);
1091+
}
1092+
1093+
if (meth_found) {
1094+
*method = descr;
1095+
return 1;
1096+
}
1097+
1098+
if (f != NULL) {
1099+
*method = f(descr, obj, (PyObject *)Py_TYPE(obj));
1100+
Py_DECREF(descr);
1101+
return 0;
1102+
}
1103+
1104+
if (descr != NULL) {
1105+
*method = descr;
1106+
return 0;
1107+
}
1108+
1109+
PyErr_Format(PyExc_AttributeError,
1110+
"'%.50s' object has no attribute '%U'",
1111+
tp->tp_name, name);
1112+
return 0;
1113+
}
1114+
1115+
/* Generic GetAttr functions - put these in your tp_[gs]etattro slot. */
10321116

10331117
PyObject *
10341118
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
10351119
{
1120+
/* Make sure the logic of _PyObject_GetMethod is in sync with
1121+
this method.
1122+
*/
1123+
10361124
PyTypeObject *tp = Py_TYPE(obj);
10371125
PyObject *descr = NULL;
10381126
PyObject *res = NULL;

PC/launcher.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@ static PYC_MAGIC magic_values[] = {
10901090
{ 3250, 3310, L"3.4" },
10911091
{ 3320, 3351, L"3.5" },
10921092
{ 3360, 3379, L"3.6" },
1093+
{ 3390, 3399, L"3.7" },
10931094
{ 0 }
10941095
};
10951096

Python/ceval.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
#define CHECKEXC 1 /* Double-check exception checking */
3333
#endif
3434

35+
/* Private API for the LOAD_METHOD opcode. */
36+
extern int _PyObject_GetMethod(PyObject *, PyObject *, PyObject **);
37+
3538
typedef PyObject *(*callproc)(PyObject *, PyObject *, PyObject *);
3639

3740
/* Forward declarations */
@@ -3419,6 +3422,100 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
34193422
DISPATCH();
34203423
}
34213424

3425+
TARGET(LOAD_METHOD) {
3426+
/* Designed to work in tamdem with CALL_METHOD. */
3427+
PyObject *name = GETITEM(names, oparg);
3428+
PyObject *obj = TOP();
3429+
PyObject *meth = NULL;
3430+
3431+
int meth_found = _PyObject_GetMethod(obj, name, &meth);
3432+
3433+
SET_TOP(meth); /* Replace `obj` on top; OK if NULL. */
3434+
if (meth == NULL) {
3435+
/* Most likely attribute wasn't found. */
3436+
Py_DECREF(obj);
3437+
goto error;
3438+
}
3439+
3440+
if (meth_found) {
3441+
/* The method object is now on top of the stack.
3442+
Push `obj` back to the stack, so that the stack
3443+
layout would be:
3444+
3445+
method | obj | arg1 | ... | argN
3446+
*/
3447+
PUSH(obj);
3448+
}
3449+
else {
3450+
/* Not a method (but a regular attr, or something
3451+
was returned by a descriptor protocol). Push
3452+
NULL to the top of the stack, to signal
3453+
CALL_METHOD that it's not a method call.
3454+
*/
3455+
Py_DECREF(obj);
3456+
PUSH(NULL);
3457+
}
3458+
DISPATCH();
3459+
}
3460+
3461+
TARGET(CALL_METHOD) {
3462+
/* Designed to work in tamdem with LOAD_METHOD. */
3463+
PyObject **sp, *res, *obj;
3464+
3465+
sp = stack_pointer;
3466+
3467+
obj = PEEK(oparg + 1);
3468+
if (obj == NULL) {
3469+
/* `obj` is NULL when LOAD_METHOD thinks that it's not
3470+
a method call. Swap the NULL and callable.
3471+
3472+
Stack layout:
3473+
3474+
... | callable | NULL | arg1 | ... | argN
3475+
^- TOP()
3476+
^- (-oparg)
3477+
^- (-oparg-1)
3478+
^- (-oparg-2)
3479+
3480+
after the next line it will be:
3481+
3482+
... | callable | callable | arg1 | ... | argN
3483+
^- TOP()
3484+
^- (-oparg)
3485+
^- (-oparg-1)
3486+
^- (-oparg-2)
3487+
3488+
Right side `callable` will be POPed by call_funtion.
3489+
Left side `callable` will be POPed manually later
3490+
(one of "callbale" refs on the stack is borrowed.)
3491+
*/
3492+
SET_VALUE(oparg + 1, PEEK(oparg + 2));
3493+
res = call_function(&sp, oparg, NULL);
3494+
stack_pointer = sp;
3495+
(void)POP(); /* POP the left side callable. */
3496+
}
3497+
else {
3498+
/* This is a method call. Stack layout:
3499+
3500+
... | method | obj | arg1 | ... | argN
3501+
^- TOP()
3502+
^- (-oparg)
3503+
^- (-oparg-1)
3504+
3505+
`obj` and `method` will be POPed by call_function.
3506+
We'll be passing `oparg + 1` to call_function, to
3507+
make it accept the `obj` as a first argument.
3508+
*/
3509+
res = call_function(&sp, oparg + 1, NULL);
3510+
stack_pointer = sp;
3511+
}
3512+
3513+
PUSH(res);
3514+
if (res == NULL)
3515+
goto error;
3516+
DISPATCH();
3517+
}
3518+
34223519
PREDICTED(CALL_FUNCTION);
34233520
TARGET(CALL_FUNCTION) {
34243521
PyObject **sp, *res;

Python/compile.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,8 @@ PyCompile_OpcodeStackEffect(int opcode, int oparg)
10401040
return -oparg;
10411041
case CALL_FUNCTION:
10421042
return -oparg;
1043+
case CALL_METHOD:
1044+
return -oparg-1;
10431045
case CALL_FUNCTION_KW:
10441046
return -oparg-1;
10451047
case CALL_FUNCTION_EX:
@@ -1078,6 +1080,8 @@ PyCompile_OpcodeStackEffect(int opcode, int oparg)
10781080
/* If there's a fmt_spec on the stack, we go from 2->1,
10791081
else 1->1. */
10801082
return (oparg & FVS_MASK) == FVS_HAVE_SPEC ? -1 : 0;
1083+
case LOAD_METHOD:
1084+
return 1;
10811085
default:
10821086
return PY_INVALID_STACK_EFFECT;
10831087
}
@@ -3399,9 +3403,42 @@ compiler_compare(struct compiler *c, expr_ty e)
33993403
return 1;
34003404
}
34013405

3406+
static int
3407+
maybe_optimize_method_call(struct compiler *c, expr_ty e)
3408+
{
3409+
Py_ssize_t argsl, i;
3410+
expr_ty meth = e->v.Call.func;
3411+
asdl_seq *args = e->v.Call.args;
3412+
3413+
/* Check that the call node is an attribute access, and that
3414+
the call doesn't have keyword parameters. */
3415+
if (meth->kind != Attribute_kind || meth->v.Attribute.ctx != Load ||
3416+
asdl_seq_LEN(e->v.Call.keywords))
3417+
return -1;
3418+
3419+
/* Check that there are no *varargs types of arguments. */
3420+
argsl = asdl_seq_LEN(args);
3421+
for (i = 0; i < argsl; i++) {
3422+
expr_ty elt = asdl_seq_GET(args, i);
3423+
if (elt->kind == Starred_kind) {
3424+
return -1;
3425+
}
3426+
}
3427+
3428+
/* Alright, we can optimize the code. */
3429+
VISIT(c, expr, meth->v.Attribute.value);
3430+
ADDOP_NAME(c, LOAD_METHOD, meth->v.Attribute.attr, names);
3431+
VISIT_SEQ(c, expr, e->v.Call.args);
3432+
ADDOP_I(c, CALL_METHOD, asdl_seq_LEN(e->v.Call.args));
3433+
return 1;
3434+
}
3435+
34023436
static int
34033437
compiler_call(struct compiler *c, expr_ty e)
34043438
{
3439+
if (maybe_optimize_method_call(c, e) > 0)
3440+
return 1;
3441+
34053442
VISIT(c, expr, e->v.Call.func);
34063443
return compiler_call_helper(c, 0,
34073444
e->v.Call.args,

0 commit comments

Comments
 (0)