Repository navigation
Broader specialization in the Specializing Adaptive Interpreter for better JIT performance #143732
Description
Until now, our choice of specialization in the SAI has been driven by performance of the interpreter alone https://github.com/python/cpython/blob/main/InternalDocs/interpreter.md#performance-analysis.
However, we now expect any further performance improvements to be provided by the JIT, not the interpreter.
This means that specializations other role, that of gathering type and branching information for the JIT, is at least as important as pure interpreter performance.
We should therefore seek to broaden specialization to gather more information, as long as it does not make interpreter performance worse, or at least no significantly so.
Using some old stats, by fraction of unspecialized bytecode executed, the top 10 were:
BINARY_OP 31.3%
FOR_ITER 19.4%
LOAD_ATTR 10.9%
STORE_SUBSCR 9.2%
BINARY_SLICE 7.3%
COMPARE_OP 7.0%
TO_BOOL 5.8%
CALL 2.5%
CONTAINS_OP 2.4%
SEND 1.7%
We should fully specialize most, if not all, of these.
In general, the above instructions have a matching __dunder__ method which determines the behavior of the operation. Recording the type of the operand(s) allows us to know what __dunder__ method is to be called.
We cannot specialize for all possible types, but we can ensure we have good inputs and type information for the JIT by adding the following two specializations for all families of instructions:
__dunder__implemented in Python. Most of the above instructions have a matching__dunder__method. These specializations should jump directly into the method.LOAD_ATTR_GETATTRIBUTE_OVERRIDDENalready does this forLOAD_ATTR. Other families should follow this template.__dunder__implemented in C. In practice, this is just the generic instruction with a bit more information recorded.
Three instructions need special casing:
- BINARY_OP. Because the behavior depends on two types, we will need a table driven approach: Specialize long tail of binary operations using a table. #100239
- BINARY_SLICE. This is supposed to avoid creating temporary slice objects for expressions like
a[b:c]but has yet to be implemented properly. There is no corresponding__dunder__method, so we would need to expose slicing methods to use. - SEND. There is no
__send__method. For iterators,__next__is called if the value isNone, otherwise.send()is called. Rather than try to replicate the specializations ofFOR_ITERwe should maybe look to combineSENDandFOR_ITERmuch like we did forCALLandCALL_METHOD
First step
Add two specializations for __dunder__ in Python and the fallback __dunder__ in C for:
- FOR_ITER
- LOAD_ATTR
- STORE_SUBSCR
- COMPARE_OP
- TO_BOOL
- CALL
- CONTAINS_OP
For a total of 12 new instructions as LOAD_ATTR already has the specialization for the Python __getattribute__ and CALL already has the generic fallback.
Second step
Implement #100239
Third step
Handle BINARY_SLICE and SEND
Linked PRs
- gh-143732: Add specialization for TO_BOOL #148113
- gh-143732: allow dict subclasses to be specialized #148128
- gh-143732: Add tier2 specialization for TO_BOOL #148271
- gh-143732: add specialization for
FOR_ITER#148745 - GH-143732: SEND specialization #148963
- gh-143732: Specialize
__setitem__dunder method forSTORE_SUBSCR#156033
Activity
Fidget-Spinner commentedon Jan 12, 2026
An alternative if we do slow down the interpreter from making it too large (very likely for computed goto/switch-case interpreter), we can just ifdef those cases out in non-JIT builds, and just reserve the opcodes for use. This way, the default build interpreter won't slow down.
kumaraditya303 commentedon Jan 12, 2026
As a data point, any coroutine runner like asyncio or trio uses
coroutine.send(arg)to iterate through the coroutine. Currently each such call goes through vectorcall to in the end setup a new frame and send arg to generator. I think it could be optimized likeCALL_COROUTINE_SENDwhere the frame can be inlined and avoid going through vectorcall.the pyperformance benchmarks uses the C implementation of asyncio and there is no trio benchmark so this case is not represented but this is an important missed specialization.
markshannon commentedon Jan 13, 2026
@kumaraditya303 We should rewrite
send()in byteocde, rather than adding even more specializations.markshannon commentedon Jan 13, 2026
👍
That should allow us to add these without worrying about interpreter-only performance too much.
Although, I'm hoping that we won't need this, as we already have far too many different configurations.
markshannon commentedon Jan 13, 2026
One other consideration is the increase in size of inline caches necessary to store the type information and possible pointers.
To specialize for a method
__dunder__we need a version number for the guard and a pointer to the__dunder__function.Rather than put the pointer to the function inline we can cache it on the class, as we already do for
__getitem__and__init__storing them in the class's_specialization_cache. If we expect specializations to be overwhelmingly builtin classes, a single 16bit cache is sufficient, but we will generally need a 32 bit (2 code units) cache.*We expect that most if not all iterators written in Python will be generators.
** No need to store the method in the
_specialization_cacheas there is space to store in inlinemarkshannon commentedon Jan 13, 2026
These are the specializations I think we should add to start with:
__dunder__in Python__dunder__in CWe can leave
BINARY_OPandCOMPARE_OPfor later, as they need more complex table-based approaches.kumaraditya303 commentedon Jan 13, 2026
Do you mean something like #142864 for send with one arg?
markshannon commentedon Jan 13, 2026
No. Rewrite send itself, not its callers.
Something like faster-cpython/ideas#448 (comment)
markshannon commentedon Feb 26, 2026
See also #145047
eendebakpt commentedon Mar 25, 2026
@markshannon I started working on the
_TO_BOOL_GENERICcase. I have a working branch TO_BOOL_GENERIC that adds the opcode_TO_BOOL_GENERIC(which gives better specialization for the JIT) and adds a tier 2 specialization_TO_BOOL_DICT(tier 2 so we do not add too many tier 1 opcodes). The new_TO_BOOL_GENERICismacro(TO_BOOL_GENERIC) = unused/1 + _RECORD_TOS_TYPE + _GUARD_TYPE_VERSION + _TO_BOOL;so is still using the "normal" conversion to bool. Would you mind having a look whether this is in the right direction?Microbenchmarks shows the effect of the
_TO_BOOL_DICT:if dct: (dict)if t: (tuple)if s: (set)if f: (float)a.append(i)If I understand your ideas correctly we should adapt the code further so that
_TO_BOOL_GENERICcalls directly the appropriate method for conversion to bool. Is that part needed or would the current approach already be good enough (since it does propagate the type information and allows jit specializations).kumaraditya303 commentedon Apr 7, 2026
@markshannon I have created #148128 to allow broader specialization of dict access for subclasses like defaultdict.
eendebakpt commentedon Apr 7, 2026
Some more recent statistics on deferred by instruction (with #128956, which affects the BINARY_OP):
2 remaining items
eendebakpt commentedon May 1, 2026
For info: a branch with specialization of
CONTAINS_OPin tier2 is main...eendebakpt:contains_op_specialization. The performance gain is a few percent on microbenchmarks, so I don't think a PR is worthwhile atm.Merge branch 'main' into pythongh-143732/for-iter-type-recording
Merge branch 'main' into pythongh-143732/for-iter-type-recording
Merge branch 'main' into pythongh-143732/for-iter-type-recording
gh-143732: allow dict subclasses to be specialized (GH-148128)
Merge branch 'main' into pythongh-143732/for-iter-type-recording
gh-143732: add specialization for `FOR_ITER` (GH-148745)
GH-143732: SEND specialization (GH-148963)
pythongh-143732: Keep the cached `__setitem__` off the value stack
pythongh-143732: Specialize `__setitem__` dunder method for `STORE_SU…
pythongh-143732: Specialize `__setitem__` dunder method for `STORE_SU…
pythongh-143732: Specialize `__setitem__` dunder method for `STORE_SU…
pythongh-143732: Specialize `__setitem__` dunder method for `STORE_SU…
pythongh-143732: Specialize `__setitem__` dunder method for `STORE_SU…