Skip to content

Commit 2ac9f12

Browse files
Document how to add a new bytecode specialization
1 parent 03909b3 commit 2ac9f12

2 files changed

Lines changed: 341 additions & 0 deletions

File tree

‎developer-workflow/index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ Development workflow
1414
extension-modules
1515
c-api
1616
grammar
17+
specialization
1718
porting
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
.. _specialization:
2+
3+
=====================================
4+
Specializing a bytecode instruction
5+
=====================================
6+
7+
CPython 3.11 and later includes a Specializing Adaptive Interpreter (:pep:`659`).
8+
The interpreter analyzes types and shapes of data at runtime and replaces generic,
9+
unspecialized bytecode instructions with faster, specialized variants tailored to
10+
specific data types or conditions (for example, lists, dictionaries, tuples, or
11+
integers).
12+
13+
When an unspecialized instruction runs repeatedly, its inline cache tracks execution
14+
frequency. Once the counter reaches zero, the interpreter attempts to *specialize*
15+
the instruction to a faster variant. If runtime conditions change or an unexpected
16+
type arrives, the instruction *deoptimizes* back to the base instruction.
17+
18+
This guide provides a step-by-step checklist for adding a new bytecode specialization
19+
to CPython, using the specialization of ``CONTAINS_OP`` from
20+
:github:`python/cpython pull request #116385 <python/cpython/pull/116385>` as a concrete
21+
example.
22+
23+
24+
Overview of components
25+
======================
26+
27+
Specializing an instruction requires updates across several parts of the CPython codebase:
28+
29+
1. **Inline cache definition** in :cpy-file:`Include/internal/pycore_code.h`:
30+
defines the memory structure used to store the adaptive counter and any cached values.
31+
2. **Instruction definition and DSL** in :cpy-file:`Python/bytecodes.c`:
32+
defines the instruction family, micro-ops (uops), specializing uop, instruction macro,
33+
and specialized instructions.
34+
3. **Specialization logic** in :cpy-file:`Python/specialize.c`:
35+
inspects runtime objects, selects the appropriate specialized opcode, and updates
36+
adaptive counters and statistics.
37+
4. **Disassembler metadata** in :cpy-file:`Lib/opcode.py`:
38+
describes inline cache layout so :mod:`dis` can decode instructions correctly.
39+
5. **Bytecode magic number** in :cpy-file:`Include/internal/pycore_magic_number.h`:
40+
invalidates outdated ``.pyc`` files across Python builds.
41+
6. **Code regeneration**:
42+
runs tools to generate C opcode headers and interpreter switch cases.
43+
44+
45+
Step-by-step checklist
46+
======================
47+
48+
The following steps use the ``in`` / ``not in`` operator instruction (``CONTAINS_OP``)
49+
as a real-world example.
50+
51+
52+
Step 1: Define the cache structure and function declaration
53+
-----------------------------------------------------------
54+
55+
Each specializable instruction family requires an inline cache. At minimum, the
56+
cache must include a 16-bit adaptive counter (``uint16_t counter``). If the
57+
specialization tracks additional state (like type versions or dictionary keys),
58+
additional fields are included in this struct.
59+
60+
In :cpy-file:`Include/internal/pycore_code.h`:
61+
62+
1. Define the cache structure and the cache entry count macro:
63+
64+
.. code-block:: c
65+
66+
typedef struct {
67+
uint16_t counter;
68+
} _PyContainsOpCache;
69+
70+
#define INLINE_CACHE_ENTRIES_CONTAINS_OP CACHE_ENTRIES(_PyContainsOpCache)
71+
72+
The ``CACHE_ENTRIES()`` macro computes the number of 2-byte code units
73+
(``_Py_CODEUNIT``) required for the struct.
74+
75+
2. Declare the specializing function:
76+
77+
.. code-block:: c
78+
79+
extern void _Py_Specialize_ContainsOp(PyObject *value, _Py_CODEUNIT *instr);
80+
81+
82+
Step 2: Update Python/bytecodes.c
83+
---------------------------------
84+
85+
In :cpy-file:`Python/bytecodes.c`, the interpreter instructions are written using
86+
CPython's bytecode Domain Specific Language (DSL).
87+
88+
1. **Define the family:**
89+
Group the base opcode and all of its specialized variants into an instruction
90+
family:
91+
92+
.. code-block:: c
93+
94+
family(CONTAINS_OP, INLINE_CACHE_ENTRIES_CONTAINS_OP) = {
95+
CONTAINS_OP_LIST,
96+
CONTAINS_OP_SET,
97+
CONTAINS_OP_TUPLE,
98+
CONTAINS_OP_DICT,
99+
CONTAINS_OP_STR,
100+
};
101+
102+
2. **Convert the base instruction to a micro-op (uop):**
103+
Change the original instruction definition from ``inst(CONTAINS_OP, ...)``
104+
to an internal uop ``op(_CONTAINS_OP, ...)``:
105+
106+
.. code-block:: c
107+
108+
op(_CONTAINS_OP, (left, right -- b)) {
109+
int res = PySequence_Contains(right, left);
110+
DECREF_INPUTS();
111+
ERROR_IF(res < 0, error);
112+
b = (res ^ oparg) ? Py_True : Py_False;
113+
}
114+
115+
3. **Add the specializing uop:**
116+
Create a specializing micro-op that checks if the adaptive counter has expired.
117+
When the counter reaches zero, call your specializing function. Otherwise,
118+
decrement the counter and record deferred statistics:
119+
120+
.. code-block:: c
121+
122+
specializing op(_SPECIALIZE_CONTAINS_OP, (counter/1, left, right -- left, right)) {
123+
#if ENABLE_SPECIALIZATION
124+
if (ADAPTIVE_COUNTER_IS_ZERO(counter)) {
125+
next_instr = this_instr;
126+
_Py_Specialize_ContainsOp(right, next_instr);
127+
DISPATCH_SAME_OPARG();
128+
}
129+
STAT_INC(CONTAINS_OP, deferred);
130+
DECREMENT_ADAPTIVE_COUNTER(this_instr[1].cache);
131+
#endif /* ENABLE_SPECIALIZATION */
132+
}
133+
134+
.. note::
135+
The ``counter/1`` notation tells the generator that this uop consumes one
136+
inline cache entry from the instruction stream.
137+
138+
4. **Define the macro:**
139+
Combine the specializing uop with the unspecialized uop to define the top-level
140+
macro instruction:
141+
142+
.. code-block:: c
143+
144+
macro(CONTAINS_OP) = _SPECIALIZE_CONTAINS_OP + _CONTAINS_OP;
145+
146+
5. **Implement specialized instructions:**
147+
Write the specialized instruction cases. Each specialized instruction begins
148+
with ``unused/N`` (where ``N`` is the cache entry count) and one or more
149+
``DEOPT_IF`` guards. If the guard succeeds, execute the optimized fast path:
150+
151+
.. code-block:: c
152+
153+
inst(CONTAINS_OP_LIST, (unused/1, left, right -- b)) {
154+
DEOPT_IF(!PyList_CheckExact(right));
155+
int res = _PyList_Contains(right, left);
156+
DECREF_INPUTS();
157+
ERROR_IF(res < 0, error);
158+
b = (res ^ oparg) ? Py_True : Py_False;
159+
}
160+
161+
inst(CONTAINS_OP_DICT, (unused/1, left, right -- b)) {
162+
DEOPT_IF(!PyDict_CheckExact(right));
163+
int res = PyDict_Contains(right, left);
164+
DECREF_INPUTS();
165+
ERROR_IF(res < 0, error);
166+
b = (res ^ oparg) ? Py_True : Py_False;
167+
}
168+
169+
(Repeat for any other specialized variants such as ``_SET``, ``_TUPLE``, or ``_STR``).
170+
171+
172+
Step 3: Implement the specializing function in Python/specialize.c
173+
------------------------------------------------------------------
174+
175+
In :cpy-file:`Python/specialize.c`, implement the function that selects the specialized
176+
opcode based on runtime types:
177+
178+
1. **Register the stats dictionary:**
179+
Inside ``_Py_GetSpecializationStats()``, call ``add_stat_dict()``:
180+
181+
.. code-block:: c
182+
183+
err += add_stat_dict(stats, CONTAINS_OP, "contains_op");
184+
185+
2. **Implement the function:**
186+
187+
.. code-block:: c
188+
189+
void
190+
_Py_Specialize_ContainsOp(PyObject *value, _Py_CODEUNIT *instr)
191+
{
192+
assert(ENABLE_SPECIALIZATION);
193+
assert(_PyOpcode_Caches[CONTAINS_OP] == INLINE_CACHE_ENTRIES_CONTAINS_OP);
194+
_PyContainsOpCache *cache = (_PyContainsOpCache *)(instr + 1);
195+
196+
if (PyUnicode_CheckExact(value)) {
197+
instr->op.code = CONTAINS_OP_STR;
198+
goto success;
199+
}
200+
if (PyList_CheckExact(value)) {
201+
instr->op.code = CONTAINS_OP_LIST;
202+
goto success;
203+
}
204+
if (PyTuple_CheckExact(value)) {
205+
instr->op.code = CONTAINS_OP_TUPLE;
206+
goto success;
207+
}
208+
if (PyDict_CheckExact(value)) {
209+
instr->op.code = CONTAINS_OP_DICT;
210+
goto success;
211+
}
212+
if (PySet_CheckExact(value)) {
213+
instr->op.code = CONTAINS_OP_SET;
214+
goto success;
215+
}
216+
217+
STAT_INC(CONTAINS_OP, failure);
218+
instr->op.code = CONTAINS_OP;
219+
cache->counter = adaptive_counter_backoff(cache->counter);
220+
return;
221+
222+
success:
223+
STAT_INC(CONTAINS_OP, success);
224+
cache->counter = adaptive_counter_cooldown();
225+
}
226+
227+
* When a suitable type is matched, rewrite ``instr->op.code``, increment the ``success``
228+
stat, and reset the counter using ``adaptive_counter_cooldown()``.
229+
* If no specialized case applies, increment the ``failure`` stat and back off the
230+
counter using ``adaptive_counter_backoff()`` to avoid repeated failing attempts.
231+
232+
233+
Step 4: Expose internal fast-path C helpers if needed
234+
-----------------------------------------------------
235+
236+
If your specialized operations need direct access to type-internal functions without
237+
generic slot lookups, declare internal helpers in :cpy-file:`Include/internal/` and
238+
define them in the corresponding object file.
239+
240+
For example, in PR #116385:
241+
242+
* Declared ``PyAPI_FUNC(int) _PyList_Contains(PyObject *aa, PyObject *el);`` in
243+
:cpy-file:`Include/internal/pycore_list.h`.
244+
* Exported the definition in :cpy-file:`Objects/listobject.c`.
245+
* Similar internal helpers were declared for sets (:cpy-file:`Include/internal/pycore_setobject.h`)
246+
and tuples (:cpy-file:`Include/internal/pycore_tuple.h`).
247+
248+
249+
Step 5: Add cache layout in Lib/opcode.py
250+
-----------------------------------------
251+
252+
In :cpy-file:`Lib/opcode.py`, add your instruction's cache format to ``_inline_cache_entries``:
253+
254+
.. code-block:: python
255+
256+
_inline_cache_entries = {
257+
...
258+
"CONTAINS_OP": {
259+
"counter": 1,
260+
},
261+
...
262+
}
263+
264+
This enables the :mod:`dis` module to recognize the cache entry words following the opcode,
265+
allowing disassembly output to format and display cache entries properly.
266+
267+
268+
Step 6: Bump the bytecode magic number
269+
--------------------------------------
270+
271+
Because adding inline cache entries alters the bytecode layout, compiled ``.pyc`` files
272+
generated by earlier builds are incompatible.
273+
274+
Bump the value of ``MAGIC_NUMBER`` in :cpy-file:`Include/internal/pycore_magic_number.h`
275+
(and note the change in comments).
276+
277+
278+
Step 7: Regenerate code files
279+
-----------------------------
280+
281+
Regenerate the opcode metadata and generated interpreter files:
282+
283+
* On Linux/macOS:
284+
285+
.. code-block:: shell
286+
287+
make regen-all
288+
289+
* On Windows:
290+
291+
.. code-block:: bat
292+
293+
build.bat --regen
294+
295+
This regenerates the following files:
296+
297+
* :cpy-file:`Include/opcode_ids.h`
298+
* :cpy-file:`Include/internal/pycore_opcode_metadata.h`
299+
* :cpy-file:`Include/internal/pycore_uop_ids.h`
300+
* :cpy-file:`Include/internal/pycore_uop_metadata.h`
301+
* :cpy-file:`Python/generated_cases.c.h`
302+
* :cpy-file:`Python/executor_cases.c.h`
303+
* :cpy-file:`Python/optimizer_cases.c.h`
304+
* :cpy-file:`Python/opcode_targets.h`
305+
* :cpy-file:`Lib/_opcode_metadata.py`
306+
307+
308+
Step 8: Testing and benchmarking
309+
--------------------------------
310+
311+
1. **Run the test suite:**
312+
313+
.. code-block:: shell
314+
315+
./python -m test test_dis test_opcodes
316+
317+
2. **Verify disassembly:**
318+
Ensure :mod:`dis` correctly shows the cache entries:
319+
320+
.. code-block:: shell
321+
322+
./python -m dis -c "x in [1, 2, 3]"
323+
324+
3. **Check specialization statistics:**
325+
Build Python with specialization statistics enabled (``--enable-pystats``)
326+
and inspect stats using ``python -X pystats`` to ensure that hits and successes
327+
are properly recorded.
328+
329+
4. **Benchmark:**
330+
Run relevant benchmarks using `pyperformance <https://pyperformance.readthedocs.io/>`__
331+
to verify that the specialization produces measurable speedups and that deoptimization
332+
penalties remain low.
333+
334+
335+
See also
336+
========
337+
338+
* :pep:`659` -- Specializing Adaptive Interpreter
339+
* :cpy-file:`InternalDocs/interpreter.md`
340+
* :github:`python/cpython PR #116385 <python/cpython/pull/116385>`: Specialization of ``CONTAINS_OP``

0 commit comments

Comments
 (0)