Skip to content

Commit 0dc8b50

Browse files
authored
gh-87729: add LOAD_SUPER_ATTR instruction for faster super() (#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.
1 parent 22bed58 commit 0dc8b50

18 files changed

+783
-408
lines changed

Doc/library/dis.rst

+18
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,24 @@ iterations of the loop.
10361036
pushed to the stack before the attribute or unbound method respectively.
10371037

10381038

1039+
.. opcode:: LOAD_SUPER_ATTR (namei)
1040+
1041+
This opcode implements :func:`super` (e.g. ``super().method()`` and
1042+
``super().attr``). It works the same as :opcode:`LOAD_ATTR`, except that
1043+
``namei`` is shifted left by 2 bits instead of 1, and instead of expecting a
1044+
single receiver on the stack, it expects three objects (from top of stack
1045+
down): ``self`` (the first argument to the current method), ``cls`` (the
1046+
class within which the current method was defined), and the global ``super``.
1047+
1048+
The low bit of ``namei`` signals to attempt a method load, as with
1049+
:opcode:`LOAD_ATTR`.
1050+
1051+
The second-low bit of ``namei``, if set, means that this was a two-argument
1052+
call to :func:`super` (unset means zero-argument).
1053+
1054+
.. versionadded:: 3.12
1055+
1056+
10391057
.. opcode:: COMPARE_OP (opname)
10401058

10411059
Performs a Boolean operation. The operation name can be found in

Include/internal/pycore_opcode.h

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

Include/internal/pycore_typeobject.h

+3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ _Py_type_getattro(PyTypeObject *type, PyObject *name);
9898
PyObject *_Py_slot_tp_getattro(PyObject *self, PyObject *name);
9999
PyObject *_Py_slot_tp_getattr_hook(PyObject *self, PyObject *name);
100100

101+
PyObject *
102+
_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *meth_found);
103+
101104
#ifdef __cplusplus
102105
}
103106
#endif

Include/opcode.h

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

Lib/dis.py

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
FOR_ITER = opmap['FOR_ITER']
4242
SEND = opmap['SEND']
4343
LOAD_ATTR = opmap['LOAD_ATTR']
44+
LOAD_SUPER_ATTR = opmap['LOAD_SUPER_ATTR']
4445

4546
CACHE = opmap["CACHE"]
4647

@@ -475,6 +476,10 @@ def _get_instructions_bytes(code, varname_from_oparg=None,
475476
argval, argrepr = _get_name_info(arg//2, get_name)
476477
if (arg & 1) and argrepr:
477478
argrepr = "NULL|self + " + argrepr
479+
elif deop == LOAD_SUPER_ATTR:
480+
argval, argrepr = _get_name_info(arg//4, get_name)
481+
if (arg & 1) and argrepr:
482+
argrepr = "NULL|self + " + argrepr
478483
else:
479484
argval, argrepr = _get_name_info(arg, get_name)
480485
elif deop in hasjabs:

Lib/importlib/_bootstrap_external.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,8 @@ def _write_atomic(path, data, mode=0o666):
439439
# Python 3.12a7 3523 (Convert COMPARE_AND_BRANCH back to COMPARE_OP)
440440
# Python 3.12a7 3524 (Shrink the BINARY_SUBSCR caches)
441441
# Python 3.12b1 3525 (Shrink the CALL caches)
442-
# Python 3.12a7 3526 (Add instrumentation support)
442+
# Python 3.12b1 3526 (Add instrumentation support)
443+
# Python 3.12b1 3527 (Optimize super() calls)
443444

444445
# Python 3.13 will start with 3550
445446

@@ -456,7 +457,7 @@ def _write_atomic(path, data, mode=0o666):
456457
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
457458
# in PC/launcher.c must also be updated.
458459

459-
MAGIC_NUMBER = (3526).to_bytes(2, 'little') + b'\r\n'
460+
MAGIC_NUMBER = (3527).to_bytes(2, 'little') + b'\r\n'
460461

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

Lib/opcode.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def pseudo_op(name, op, real_ops):
196196
def_op('DELETE_DEREF', 139)
197197
hasfree.append(139)
198198
jrel_op('JUMP_BACKWARD', 140) # Number of words to skip (backwards)
199-
199+
name_op('LOAD_SUPER_ATTR', 141)
200200
def_op('CALL_FUNCTION_EX', 142) # Flags
201201

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

266266
pseudo_op('LOAD_METHOD', 262, ['LOAD_ATTR'])
267+
pseudo_op('LOAD_SUPER_METHOD', 263, ['LOAD_SUPER_ATTR'])
268+
pseudo_op('LOAD_ZERO_SUPER_METHOD', 264, ['LOAD_SUPER_ATTR'])
269+
pseudo_op('LOAD_ZERO_SUPER_ATTR', 265, ['LOAD_SUPER_ATTR'])
267270

268271
MAX_PSEUDO_OPCODE = MIN_PSEUDO_OPCODE + len(_pseudo_ops) - 1
269272

Lib/test/shadowed_super.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class super:
2+
msg = "truly super"
3+
4+
5+
class C:
6+
def method(self):
7+
return super().msg

Lib/test/test_gdb.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ def test_wrapper_call(self):
962962
cmd = textwrap.dedent('''
963963
class MyList(list):
964964
def __init__(self):
965-
super().__init__() # wrapper_call()
965+
super(*[]).__init__() # wrapper_call()
966966
967967
id("first break point")
968968
l = MyList()

Lib/test/test_super.py

+88-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Unit tests for zero-argument super() & related machinery."""
22

33
import unittest
4+
from unittest.mock import patch
5+
from test import shadowed_super
46

57

68
class A:
@@ -283,17 +285,28 @@ def f(self):
283285
def test_obscure_super_errors(self):
284286
def f():
285287
super()
286-
self.assertRaises(RuntimeError, f)
288+
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
289+
f()
290+
291+
class C:
292+
def f():
293+
super()
294+
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
295+
C.f()
296+
287297
def f(x):
288298
del x
289299
super()
290-
self.assertRaises(RuntimeError, f, None)
300+
with self.assertRaisesRegex(RuntimeError, r"arg\[0\] deleted"):
301+
f(None)
302+
291303
class X:
292304
def f(x):
293305
nonlocal __class__
294306
del __class__
295307
super()
296-
self.assertRaises(RuntimeError, X().f)
308+
with self.assertRaisesRegex(RuntimeError, r"empty __class__ cell"):
309+
X().f()
297310

298311
def test_cell_as_self(self):
299312
class X:
@@ -325,6 +338,78 @@ def test_super_argtype(self):
325338
with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
326339
super(1, int)
327340

341+
def test_shadowed_global(self):
342+
self.assertEqual(shadowed_super.C().method(), "truly super")
343+
344+
def test_shadowed_local(self):
345+
class super:
346+
msg = "quite super"
347+
348+
class C:
349+
def method(self):
350+
return super().msg
351+
352+
self.assertEqual(C().method(), "quite super")
353+
354+
def test_shadowed_dynamic(self):
355+
class MySuper:
356+
msg = "super super"
357+
358+
class C:
359+
def method(self):
360+
return super().msg
361+
362+
with patch("test.test_super.super", MySuper) as m:
363+
self.assertEqual(C().method(), "super super")
364+
365+
def test_shadowed_dynamic_two_arg(self):
366+
call_args = []
367+
class MySuper:
368+
def __init__(self, *args):
369+
call_args.append(args)
370+
msg = "super super"
371+
372+
class C:
373+
def method(self):
374+
return super(1, 2).msg
375+
376+
with patch("test.test_super.super", MySuper) as m:
377+
self.assertEqual(C().method(), "super super")
378+
self.assertEqual(call_args, [(1, 2)])
379+
380+
def test_attribute_error(self):
381+
class C:
382+
def method(self):
383+
return super().msg
384+
385+
with self.assertRaisesRegex(AttributeError, "'super' object has no attribute 'msg'"):
386+
C().method()
387+
388+
def test_bad_first_arg(self):
389+
class C:
390+
def method(self):
391+
return super(1, self).method()
392+
393+
with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
394+
C().method()
395+
396+
def test_super___class__(self):
397+
class C:
398+
def method(self):
399+
return super().__class__
400+
401+
self.assertEqual(C().method(), super)
402+
403+
def test_super_subclass___class__(self):
404+
class mysuper(super):
405+
pass
406+
407+
class C:
408+
def method(self):
409+
return mysuper(C, self).__class__
410+
411+
self.assertEqual(C().method(), mysuper)
412+
328413

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

0 commit comments

Comments
 (0)