Repository navigation
Use dedicated opcodes to speed up calls/attribute lookups with super() as receiver #87729
Description
Activity
Calling methods and lookup up attributes when receiver is
super()has extra cost comparing to regular attribute lookup. It mainly comes from the need to allocate and initialize the instance of thesuperwhich for zero argument case also include peeking into frame/code object for the__class__cell and first argument. In addition becausePySuper_Typehas custom implementation of tp_getattro -_PyObject_GetMethodwould always return bound method.import timeit setup = """ class A: def f(self): pass class B(A): def f(self): super().f() def g(self): A.f(self) b = B() """ print(timeit.timeit("b.f()", setup=setup, number=20000000)) print(timeit.timeit("b.g()", setup=setup, number=20000000)) 7.329449548968114 3.892987059080042One option to improve it could be to make compiler/interpreter aware of super calls so they can be treated specially. Attached patch introduces two new opcodes LOAD_METHOD_SUPER and LOAD_ATTR_SUPER that are intended to be counterparts for LOAD_METHOD and LOAD_ATTR for cases when receiver is super with either zero or two arguments.
Immediate argument for both LOAD_METHOD_SUPER and LOAD_ATTR_SUPER is a pair that consist of:
0: index of method/attribute in co_names
1: Py_True if super was originally called with 0 arguments and Py_False otherwise.Both LOAD_METHOD_SUPER and LOAD_ATTR_SUPER expect 3 elements on the stack:
TOS3: global_super
TOS2: type
TOS1: self/clsResult of LOAD_METHOD_SUPER is the same as LOAD_METHOD.
Result of LOAD_ATTR_SUPER is the same as LOAD_ATTRIn runtime both LOAD_METHOD_SUPER and LOAD_ATTR_SUPER will check if
global_superisPySuper_Typeto handle situations whensuperis patched. Ifglobal_superisPySuper_Typethen it can use dedicated routine to perform the lookup for provided__class__andcls/selfwithout allocating newsuperinstance. Ifglobal_superis different fromPySuper_Typethen runtime will fallback to the original logic usingglobal_superand original number of arguments that was captured in immediate.Benchmark results with patch:
4.381768501014449
3.9492998640052974- added3.10 (EOL)end of lifeend of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
on Mar 19, 2021 Why?
Do you have any evidence that the overhead of super() is significant in real programs, or that the proposed change actually speeds up anything beyond your micro-benchmark?
Currently, super() is decoupled from the core language. It is just a builtin that provides customized attribute lookup. This PR makes super() more tightly integrated with the core language, treating it as if it were a keyword and part of the grammar. Also note, users can currently create their own versions of super(), shadowing the builtin super().
Currently, super() is decoupled from the core language. It is just a builtin that provides customized attribute lookup. This PR makes super() more tightly integrated with the core language, treating it as if it were a keyword and part of the grammar. Also note, users can currently create their own versions of super(), shadowing the builtin super().
This is true however:
- this patch does not block people from introducing custom version of
superso this scenario still work. The idea was to streamline the common case - based on digging into Instagram codebase and its transitive dependencies (which is reasonably large amount of code) all spots where
super()appear in sources assumesuperto be builtin and for a pretty common use-case its cost is noticeable in profiler. - zero-argument
super()still a bit magical since it requires compiler support to create cell for__class__and assumes certain shape of the frame object so this patch is a step forward with a better compiler support and removing runtime dependency on the frame
Do you have any evidence that the overhead of super() is significant in real programs
I do see the non-negligible cost of allocation/initialization of
superobject in IG profiling data.- this patch does not block people from introducing custom version of
This looks like a sensible idea to me. The safeguards to ensure that customized 'super' still works seem reasonable to me. I have to admit that I sometimes refrain from using super() where I should because of the expense, so this would be welcome.
I do wonder -- is two-arg super() important enough to support it at all? Maybe the code would be somewhat simpler if the special opcodes were only generated for zero-arg super() calls.
Numbers please.
What is "non-negligible cost of allocation/initialization" mean as a fraction of runtime?
What sort of speed up are you seeing on whole programs?Apologies for the delay in reply: in more concrete numbers for IG codebase enabling this optimization resulted in 0.2% CPU win.
- added a commit that references this issue
on Apr 13, 2023 - added a commit that references this issue
on Apr 25, 2023
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs