|
| 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