Skip to content

Commit

Permalink
pythongh-87729: add LOAD_SUPER_ATTR instruction for faster super() (p…
Browse files Browse the repository at this point in the history
…ython#103497)

This speeds up `super()` (by around 85%, for a simple one-level
`super().meth()` microbenchmark) by avoiding allocation of a new
single-use `super()` object on each use.
  • Loading branch information
carljm authored Apr 24, 2023
1 parent 22bed58 commit 0dc8b50
Show file tree
Hide file tree
Showing 18 changed files with 783 additions and 408 deletions.
18 changes: 18 additions & 0 deletions Doc/library/dis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,24 @@ iterations of the loop.
pushed to the stack before the attribute or unbound method respectively.


.. opcode:: LOAD_SUPER_ATTR (namei)

This opcode implements :func:`super` (e.g. ``super().method()`` and
``super().attr``). It works the same as :opcode:`LOAD_ATTR`, except that
``namei`` is shifted left by 2 bits instead of 1, and instead of expecting a
single receiver on the stack, it expects three objects (from top of stack
down): ``self`` (the first argument to the current method), ``cls`` (the
class within which the current method was defined), and the global ``super``.

The low bit of ``namei`` signals to attempt a method load, as with
:opcode:`LOAD_ATTR`.

The second-low bit of ``namei``, if set, means that this was a two-argument
call to :func:`super` (unset means zero-argument).

.. versionadded:: 3.12


.. opcode:: COMPARE_OP (opname)

Performs a Boolean operation. The operation name can be found in
Expand Down
15 changes: 9 additions & 6 deletions Include/internal/pycore_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 Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ _Py_type_getattro(PyTypeObject *type, PyObject *name);
PyObject *_Py_slot_tp_getattro(PyObject *self, PyObject *name);
PyObject *_Py_slot_tp_getattr_hook(PyObject *self, PyObject *name);

PyObject *
_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *meth_found);

#ifdef __cplusplus
}
#endif
Expand Down
23 changes: 15 additions & 8 deletions Include/opcode.h

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

5 changes: 5 additions & 0 deletions Lib/dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
FOR_ITER = opmap['FOR_ITER']
SEND = opmap['SEND']
LOAD_ATTR = opmap['LOAD_ATTR']
LOAD_SUPER_ATTR = opmap['LOAD_SUPER_ATTR']

CACHE = opmap["CACHE"]

Expand Down Expand Up @@ -475,6 +476,10 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
argval, argrepr = _get_name_info(arg//2, get_name)
if (arg & 1) and argrepr:
argrepr = "NULL|self + " + argrepr
elif deop == LOAD_SUPER_ATTR:
argval, argrepr = _get_name_info(arg//4, get_name)
if (arg & 1) and argrepr:
argrepr = "NULL|self + " + argrepr
else:
argval, argrepr = _get_name_info(arg, get_name)
elif deop in hasjabs:
Expand Down
5 changes: 3 additions & 2 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,8 @@ def _write_atomic(path, data, mode=0o666):
# Python 3.12a7 3523 (Convert COMPARE_AND_BRANCH back to COMPARE_OP)
# Python 3.12a7 3524 (Shrink the BINARY_SUBSCR caches)
# Python 3.12b1 3525 (Shrink the CALL caches)
# Python 3.12a7 3526 (Add instrumentation support)
# Python 3.12b1 3526 (Add instrumentation support)
# Python 3.12b1 3527 (Optimize super() calls)

# Python 3.13 will start with 3550

Expand All @@ -456,7 +457,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 = (3526).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3527).to_bytes(2, 'little') + b'\r\n'

_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

Expand Down
5 changes: 4 additions & 1 deletion Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def pseudo_op(name, op, real_ops):
def_op('DELETE_DEREF', 139)
hasfree.append(139)
jrel_op('JUMP_BACKWARD', 140) # Number of words to skip (backwards)

name_op('LOAD_SUPER_ATTR', 141)
def_op('CALL_FUNCTION_EX', 142) # Flags

def_op('EXTENDED_ARG', 144)
Expand Down Expand Up @@ -264,6 +264,9 @@ def pseudo_op(name, op, real_ops):
pseudo_op('JUMP_NO_INTERRUPT', 261, ['JUMP_FORWARD', 'JUMP_BACKWARD_NO_INTERRUPT'])

pseudo_op('LOAD_METHOD', 262, ['LOAD_ATTR'])
pseudo_op('LOAD_SUPER_METHOD', 263, ['LOAD_SUPER_ATTR'])
pseudo_op('LOAD_ZERO_SUPER_METHOD', 264, ['LOAD_SUPER_ATTR'])
pseudo_op('LOAD_ZERO_SUPER_ATTR', 265, ['LOAD_SUPER_ATTR'])

MAX_PSEUDO_OPCODE = MIN_PSEUDO_OPCODE + len(_pseudo_ops) - 1

Expand Down
7 changes: 7 additions & 0 deletions Lib/test/shadowed_super.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class super:
msg = "truly super"


class C:
def method(self):
return super().msg
2 changes: 1 addition & 1 deletion Lib/test/test_gdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ def test_wrapper_call(self):
cmd = textwrap.dedent('''
class MyList(list):
def __init__(self):
super().__init__() # wrapper_call()
super(*[]).__init__() # wrapper_call()
id("first break point")
l = MyList()
Expand Down
91 changes: 88 additions & 3 deletions Lib/test/test_super.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Unit tests for zero-argument super() & related machinery."""

import unittest
from unittest.mock import patch
from test import shadowed_super


class A:
Expand Down Expand Up @@ -283,17 +285,28 @@ def f(self):
def test_obscure_super_errors(self):
def f():
super()
self.assertRaises(RuntimeError, f)
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
f()

class C:
def f():
super()
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
C.f()

def f(x):
del x
super()
self.assertRaises(RuntimeError, f, None)
with self.assertRaisesRegex(RuntimeError, r"arg\[0\] deleted"):
f(None)

class X:
def f(x):
nonlocal __class__
del __class__
super()
self.assertRaises(RuntimeError, X().f)
with self.assertRaisesRegex(RuntimeError, r"empty __class__ cell"):
X().f()

def test_cell_as_self(self):
class X:
Expand Down Expand Up @@ -325,6 +338,78 @@ def test_super_argtype(self):
with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
super(1, int)

def test_shadowed_global(self):
self.assertEqual(shadowed_super.C().method(), "truly super")

def test_shadowed_local(self):
class super:
msg = "quite super"

class C:
def method(self):
return super().msg

self.assertEqual(C().method(), "quite super")

def test_shadowed_dynamic(self):
class MySuper:
msg = "super super"

class C:
def method(self):
return super().msg

with patch("test.test_super.super", MySuper) as m:
self.assertEqual(C().method(), "super super")

def test_shadowed_dynamic_two_arg(self):
call_args = []
class MySuper:
def __init__(self, *args):
call_args.append(args)
msg = "super super"

class C:
def method(self):
return super(1, 2).msg

with patch("test.test_super.super", MySuper) as m:
self.assertEqual(C().method(), "super super")
self.assertEqual(call_args, [(1, 2)])

def test_attribute_error(self):
class C:
def method(self):
return super().msg

with self.assertRaisesRegex(AttributeError, "'super' object has no attribute 'msg'"):
C().method()

def test_bad_first_arg(self):
class C:
def method(self):
return super(1, self).method()

with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
C().method()

def test_super___class__(self):
class C:
def method(self):
return super().__class__

self.assertEqual(C().method(), super)

def test_super_subclass___class__(self):
class mysuper(super):
pass

class C:
def method(self):
return mysuper(C, self).__class__

self.assertEqual(C().method(), mysuper)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :opcode:`LOAD_SUPER_ATTR` to speed up ``super().meth()`` and ``super().attr`` calls.
Loading

0 comments on commit 0dc8b50

Please sign in to comment.