Skip to content
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

bpo-46564: Optimize super().meth() calls via adaptive superinstructions #30992

Closed
wants to merge 11 commits into from
6 changes: 5 additions & 1 deletion Include/internal/pycore_code.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ cache_backoff(_PyAdaptiveEntry *entry) {
entry->counter = ADAPTIVE_CACHE_BACKOFF;
}

/* _interpreter_frame is defined in pycore_frame.h */
typedef struct _interpreter_frame InterpreterFrame;

/* Specialization functions */

int _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name, SpecializedCacheEntry *cache);
Expand All @@ -272,7 +275,8 @@ int _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *na
int _Py_Specialize_BinarySubscr(PyObject *sub, PyObject *container, _Py_CODEUNIT *instr, SpecializedCacheEntry *cache);
int _Py_Specialize_StoreSubscr(PyObject *container, PyObject *sub, _Py_CODEUNIT *instr);
int _Py_Specialize_CallNoKw(PyObject *callable, _Py_CODEUNIT *instr, int nargs,
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins);
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins,
PyObject **stack_pointer, InterpreterFrame *frame, PyObject *names);
Comment on lines +278 to +279
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins,
PyObject **stack_pointer, InterpreterFrame *frame, PyObject *names);
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins,
PyObject **stack_pointer, InterpreterFrame *frame, PyObject *names);

as in a removed line, or even:

Suggested change
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins,
PyObject **stack_pointer, InterpreterFrame *frame, PyObject *names);
PyObject *kwnames, SpecializedCacheEntry *cache, PyObject *builtins,
PyObject **stack_pointer, InterpreterFrame *frame, PyObject *names);

as in _Py_Specialize_BinaryOp right below.

void _Py_Specialize_BinaryOp(PyObject *lhs, PyObject *rhs, _Py_CODEUNIT *instr,
SpecializedCacheEntry *cache);
void _Py_Specialize_CompareOp(PyObject *lhs, PyObject *rhs, _Py_CODEUNIT *instr, SpecializedCacheEntry *cache);
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ extern PyStatus _PyTypes_InitSlotDefs(void);

extern void _PyStaticType_Dealloc(PyTypeObject *type);

/* _interpreter_frame is defined in pycore_frame.h */
typedef struct _interpreter_frame InterpreterFrame;

PyObject *_PySuper_Lookup(PyTypeObject *, PyObject *, PyObject *, int *);
int _PySuper_GetTypeArgs(InterpreterFrame *, PyCodeObject *, PyTypeObject **, PyObject **);

#ifdef __cplusplus
}
Expand Down
2 changes: 2 additions & 0 deletions Include/opcode.h

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

3 changes: 3 additions & 0 deletions Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ def jabs_op(name, op):
"LOAD_FAST__LOAD_CONST",
"LOAD_CONST__LOAD_FAST",
"STORE_FAST__STORE_FAST",
# Specialized super instructions.
"CALL_NO_KW_SUPER_0__LOAD_METHOD_CACHED",
"CALL_NO_KW_SUPER_2__LOAD_METHOD_CACHED",
]
_specialization_stats = [
"success",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Method calls on :class:`super` are sped up. The 2-argument form,
``super(type, obj).meth()`` is now nearly as fast as an equivalent
``self.meth()`` call. The 0-argument form, while still slower, is still
faster than in previous versions of CPython. Patch by Ken Jin, with
additional contributions by Vladimir Matveev.
73 changes: 54 additions & 19 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -8846,16 +8846,18 @@ super_repr(PyObject *self)
"<super: <class '%s'>, NULL>",
su->type ? su->type->tp_name : "NULL");
}
/* Forward */
static PyTypeObject *supercheck(PyTypeObject *type, PyObject *obj);

static PyObject *
super_getattro(PyObject *self, PyObject *name)
do_super_lookup(superobject *su, PyTypeObject *su_type, PyObject *su_obj,
PyTypeObject *su_obj_type, PyObject *name, int *meth_found)
{
superobject *su = (superobject *)self;
PyTypeObject *starttype;
PyObject *mro;
Py_ssize_t i, n;

starttype = su->obj_type;
starttype = su_obj_type;
if (starttype == NULL)
goto skip;

Expand All @@ -8875,7 +8877,7 @@ super_getattro(PyObject *self, PyObject *name)

/* No need to check the last one: it's gonna be skipped anyway. */
for (i = 0; i+1 < n; i++) {
if ((PyObject *)(su->type) == PyTuple_GET_ITEM(mro, i))
if ((PyObject *)(su_type) == PyTuple_GET_ITEM(mro, i))
break;
}
i++; /* skip su->type (if any) */
Expand All @@ -8893,17 +8895,25 @@ super_getattro(PyObject *self, PyObject *name)
PyObject *res = PyDict_GetItemWithError(dict, name);
if (res != NULL) {
Py_INCREF(res);

descrgetfunc f = Py_TYPE(res)->tp_descr_get;
if (f != NULL) {
PyObject *res2;
res2 = f(res,
/* Only pass 'obj' param if this is instance-mode super
(See SF ID #743627) */
(su->obj == (PyObject *)starttype) ? NULL : su->obj,
(PyObject *)starttype);
Py_DECREF(res);
res = res2;
if (meth_found &&
_PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
*meth_found = 1;
}
else {
if (meth_found) {
*meth_found = 0;
}
descrgetfunc f = Py_TYPE(res)->tp_descr_get;
if (f != NULL) {
PyObject *res2;
res2 = f(res,
/* Only pass 'obj' param if this is instance-mode super
(See SF ID #743627) */
(su_obj == (PyObject *)starttype) ? NULL : su_obj,
(PyObject *)starttype);
Py_DECREF(res);
res = res2;
}
}

Py_DECREF(mro);
Expand All @@ -8919,7 +8929,31 @@ super_getattro(PyObject *self, PyObject *name)
Py_DECREF(mro);

skip:
return PyObject_GenericGetAttr(self, name);
/* only happens when using manual _PySuper_Lookup, never happens in super_getattro */
if (su == NULL) {
PyErr_BadInternalCall();
return NULL;
}
return PyObject_GenericGetAttr((PyObject *)su, name);
}

static PyObject *
super_getattro(PyObject *self, PyObject *name)
{
superobject *su = (superobject *)self;
return do_super_lookup(su, su->type, su->obj, su->obj_type, name, NULL);
}

PyObject *
_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *meth_found)
{
PyTypeObject *starttype = supercheck(su_type, su_obj);
if (starttype == NULL) {
return NULL;
}
PyObject *res = do_super_lookup(NULL, su_type, su_obj, starttype, name, meth_found);
Py_DECREF(starttype);
return res;
}

static PyTypeObject *
Expand Down Expand Up @@ -9011,8 +9045,8 @@ super_descr_get(PyObject *self, PyObject *obj, PyObject *type)
}
}

static int
super_init_without_args(InterpreterFrame *cframe, PyCodeObject *co,
int
_PySuper_GetTypeArgs(InterpreterFrame *cframe, PyCodeObject *co,
PyTypeObject **type_p, PyObject **obj_p)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PyTypeObject **type_p, PyObject **obj_p)
PyTypeObject **type_p, PyObject **obj_p)

The line was aligned with an opening parenthesis of a parameter list.

{
if (co->co_argcount == 0) {
Expand Down Expand Up @@ -9102,7 +9136,8 @@ super_init(PyObject *self, PyObject *args, PyObject *kwds)
"super(): no current frame");
return -1;
}
int res = super_init_without_args(cframe, cframe->f_code, &type, &obj);

int res = _PySuper_GetTypeArgs(cframe, cframe->f_code, &type, &obj);

if (res < 0) {
return -1;
Expand Down
87 changes: 86 additions & 1 deletion Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,7 @@ eval_frame_handle_pending(PyThreadState *tstate)

/* The integer overflow is checked by an assertion below. */
#define INSTR_OFFSET() ((int)(next_instr - first_instr))
#define NEXT_INSTR_OFFSET() ((int)(next_instr+1 - first_instr))
#define NEXTOPARG() do { \
_Py_CODEUNIT word = *next_instr; \
opcode = _Py_OPCODE(word); \
Expand Down Expand Up @@ -1486,6 +1487,9 @@ eval_frame_handle_pending(PyThreadState *tstate)
#define GET_CACHE() \
_GetSpecializedCacheEntryForInstruction(first_instr, INSTR_OFFSET(), oparg)

# define GET_NEXT_INSTR_CACHE() \
_GetSpecializedCacheEntryForInstruction(first_instr, NEXT_INSTR_OFFSET(), \
_Py_OPARG(*next_instr))

#define DEOPT_IF(cond, instname) if (cond) { goto instname ## _miss; }

Expand Down Expand Up @@ -4633,7 +4637,8 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr
int nargs = call_shape.total_args;
int err = _Py_Specialize_CallNoKw(
call_shape.callable, next_instr, nargs,
call_shape.kwnames, cache, BUILTINS());
call_shape.kwnames, cache, BUILTINS(),
stack_pointer, frame, names);
if (err < 0) {
goto error;
}
Expand Down Expand Up @@ -5070,6 +5075,86 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, InterpreterFrame *frame, int thr
DISPATCH();
}

TARGET(CALL_NO_KW_SUPER_0__LOAD_METHOD_CACHED) {
/* super().meth */
assert(_Py_OPCODE(next_instr[0]) == LOAD_METHOD_ADAPTIVE);
assert(_Py_OPCODE(next_instr[-2]) != PRECALL_METHOD);
SpecializedCacheEntry *caches = GET_CACHE();
_PyAdaptiveEntry *cache0 = &caches[0].adaptive;
_PyObjectCache *cache1 = &caches[-1].obj;
_PyAdaptiveEntry *lm_adaptive = &caches[-2].adaptive;
Fidget-Spinner marked this conversation as resolved.
Show resolved Hide resolved
assert(lm_adaptive == &GET_NEXT_INSTR_CACHE()[0].adaptive);
assert(call_shape.total_args == 0);

/* CALL_NO_KW_SUPER */
PyObject *su_obj;
PyTypeObject *su_type;
PyObject *meth;
PyObject *super_callable = TOP();

DEOPT_IF(_PyType_CAST(super_callable) != &PySuper_Type, CALL);
/* super() - zero argument form */
if (_PySuper_GetTypeArgs(frame, frame->f_code, &su_type, &su_obj) < 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we do this at specialization time? The number of locals, the index of "self", and whether it is a cell are all known then. Likewise the nature of __class__ is also known.

PyErr_Clear();
DEOPT_IF(1, CALL);
}
assert(su_obj != NULL);
DEOPT_IF(lm_adaptive->version != Py_TYPE(su_obj)->tp_version_tag, CALL);
DEOPT_IF(cache0->version != su_type->tp_version_tag, CALL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this fail?
Isn't the next item in the MRO determined solely by type(self) and __class__, both of which are known at this point?

Copy link
Member Author

@Fidget-Spinner Fidget-Spinner Jan 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted assurance that __class__ didn't change.. Then again, I'm not sure if it can?

STAT_INC(CALL, hit);

/* LOAD_METHOD_CACHED */
meth = cache1->obj;
assert(meth != NULL && _PyType_HasFeature(Py_TYPE(meth), Py_TPFLAGS_METHOD_DESCRIPTOR));
Py_INCREF(meth);
SET_TOP(meth);
Py_INCREF(su_obj);
PUSH(su_obj);

Py_DECREF(super_callable);
next_instr++;
DISPATCH();
}

TARGET(CALL_NO_KW_SUPER_2__LOAD_METHOD_CACHED) {
/* super(type, obj).meth */
assert(_Py_OPCODE(next_instr[0]) == LOAD_METHOD_ADAPTIVE);
assert(_Py_OPCODE(next_instr[-2]) != PRECALL_METHOD);
SpecializedCacheEntry *caches = GET_CACHE();
_PyAdaptiveEntry *cache0 = &caches[0].adaptive;
_PyObjectCache *cache1 = &caches[-1].obj;
_PyAdaptiveEntry *lm_adaptive = &caches[-2].adaptive;
assert(lm_adaptive == &GET_NEXT_INSTR_CACHE()[0].adaptive);
assert(call_shape.total_args == 2);
assert(call_shape.kwnames == NULL);

/* CALL_NO_KW_SUPER */
/* super(type, obj) - two argument form */
PyObject *su_obj = TOP();
PyTypeObject *su_type = _PyType_CAST(SECOND());
PyObject *super_callable = THIRD();
PyObject *meth;

DEOPT_IF(_PyType_CAST(super_callable) != &PySuper_Type, CALL);
assert(su_obj != NULL);
DEOPT_IF(lm_adaptive->version != Py_TYPE(su_obj)->tp_version_tag, CALL);
DEOPT_IF(cache0->version != su_type->tp_version_tag, CALL);
STAT_INC(CALL, hit);

(void)(POP());
/* LOAD_METHOD_CACHED */
meth = cache1->obj;
assert(meth != NULL && _PyType_HasFeature(Py_TYPE(meth), Py_TPFLAGS_METHOD_DESCRIPTOR));
Py_INCREF(meth);
SET_SECOND(meth);
SET_TOP(su_obj);

Py_DECREF(super_callable);
Py_DECREF(su_type);
next_instr++;
DISPATCH();
}

TARGET(CALL_FUNCTION_EX) {
PREDICTED(CALL_FUNCTION_EX);
PyObject *func, *callargs, *kwargs = NULL, *result;
Expand Down
4 changes: 2 additions & 2 deletions Python/opcode_targets.h

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

Loading