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

Commit f239213

Browse files
committed
Issue python#26110: Add LOAD_METHOD/CALL_METHOD opcodes.
Special thanks to INADA Naoki for pushing the patch through the last mile, Serhiy Storchaka for reviewing the code, and to Victor Stinner for suggesting the idea (originally implemented in the PyPy project).
1 parent e6bb53b commit f239213

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
@@ -1025,11 +1025,99 @@ _PyObject_NextNotImplemented(PyObject *self)
10251025
return NULL;
10261026
}
10271027

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

10301114
PyObject *
10311115
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
10321116
{
1117+
/* Make sure the logic of _PyObject_GetMethod is in sync with
1118+
this method.
1119+
*/
1120+
10331121
PyTypeObject *tp = Py_TYPE(obj);
10341122
PyObject *descr = NULL;
10351123
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
@@ -30,6 +30,9 @@
3030
#define CHECKEXC 1 /* Double-check exception checking */
3131
#endif
3232

33+
/* Private API for the LOAD_METHOD opcode. */
34+
extern int _PyObject_GetMethod(PyObject *, PyObject *, PyObject **);
35+
3336
typedef PyObject *(*callproc)(PyObject *, PyObject *, PyObject *);
3437

3538
/* Forward declarations */
@@ -3225,6 +3228,100 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
32253228
DISPATCH();
32263229
}
32273230

3231+
TARGET(LOAD_METHOD) {
3232+
/* Designed to work in tamdem with CALL_METHOD. */
3233+
PyObject *name = GETITEM(names, oparg);
3234+
PyObject *obj = TOP();
3235+
PyObject *meth = NULL;
3236+
3237+
int meth_found = _PyObject_GetMethod(obj, name, &meth);
3238+
3239+
SET_TOP(meth); /* Replace `obj` on top; OK if NULL. */
3240+
if (meth == NULL) {
3241+
/* Most likely attribute wasn't found. */
3242+
Py_DECREF(obj);
3243+
goto error;
3244+
}
3245+
3246+
if (meth_found) {
3247+
/* The method object is now on top of the stack.
3248+
Push `obj` back to the stack, so that the stack
3249+
layout would be:
3250+
3251+
method | obj | arg1 | ... | argN
3252+
*/
3253+
PUSH(obj);
3254+
}
3255+
else {
3256+
/* Not a method (but a regular attr, or something
3257+
was returned by a descriptor protocol). Push
3258+
NULL to the top of the stack, to signal
3259+
CALL_METHOD that it's not a method call.
3260+
*/
3261+
Py_DECREF(obj);
3262+
PUSH(NULL);
3263+
}
3264+
DISPATCH();
3265+
}
3266+
3267+
TARGET(CALL_METHOD) {
3268+
/* Designed to work in tamdem with LOAD_METHOD. */
3269+
PyObject **sp, *res, *obj;
3270+
3271+
sp = stack_pointer;
3272+
3273+
obj = PEEK(oparg + 1);
3274+
if (obj == NULL) {
3275+
/* `obj` is NULL when LOAD_METHOD thinks that it's not
3276+
a method call. Swap the NULL and callable.
3277+
3278+
Stack layout:
3279+
3280+
... | callable | NULL | arg1 | ... | argN
3281+
^- TOP()
3282+
^- (-oparg)
3283+
^- (-oparg-1)
3284+
^- (-oparg-2)
3285+
3286+
after the next line it will be:
3287+
3288+
... | callable | callable | arg1 | ... | argN
3289+
^- TOP()
3290+
^- (-oparg)
3291+
^- (-oparg-1)
3292+
^- (-oparg-2)
3293+
3294+
Right side `callable` will be POPed by call_funtion.
3295+
Left side `callable` will be POPed manually later
3296+
(one of "callbale" refs on the stack is borrowed.)
3297+
*/
3298+
SET_VALUE(oparg + 1, PEEK(oparg + 2));
3299+
res = call_function(&sp, oparg, NULL);
3300+
stack_pointer = sp;
3301+
(void)POP(); /* POP the left side callable. */
3302+
}
3303+
else {
3304+
/* This is a method call. Stack layout:
3305+
3306+
... | method | obj | arg1 | ... | argN
3307+
^- TOP()
3308+
^- (-oparg)
3309+
^- (-oparg-1)
3310+
3311+
`obj` and `method` will be POPed by call_function.
3312+
We'll be passing `oparg + 1` to call_function, to
3313+
make it accept the `obj` as a first argument.
3314+
*/
3315+
res = call_function(&sp, oparg + 1, NULL);
3316+
stack_pointer = sp;
3317+
}
3318+
3319+
PUSH(res);
3320+
if (res == NULL)
3321+
goto error;
3322+
DISPATCH();
3323+
}
3324+
32283325
PREDICTED(CALL_FUNCTION);
32293326
TARGET(CALL_FUNCTION) {
32303327
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)