|
| 1 | +.. _bytecode-specialization: |
| 2 | + |
| 3 | +================================ |
| 4 | +Adding a Bytecode Specialization |
| 5 | +================================ |
| 6 | + |
| 7 | +CPython uses an adaptive, specializing interpreter (PEP 659) to optimize bytecode execution dynamically at runtime. When generic opcodes (such as ``CONTAINS_OP`` or ``BINARY_OP``) execute frequently with predictable operand types, CPython morphs them into specialized fast-path micro-ops (uops) to bypass generic type dispatch. |
| 8 | + |
| 9 | +This guide describes the step-by-step process of introducing a new bytecode specialization in CPython. |
| 10 | + |
| 11 | +.. note:: |
| 12 | + |
| 13 | + This guide uses the specialization of ``CONTAINS_OP`` (from `CPython PR #116385 <https://github.com/python/cpython/pull/116385>`_) as a reference example. |
| 14 | + |
| 15 | + |
| 16 | +Overview of Steps |
| 17 | +----------------- |
| 18 | + |
| 19 | +Adding a new bytecode specialization involves coordinated changes across several files in the CPython source tree: |
| 20 | + |
| 21 | +1. :ref:`spec-step-bytecodes` |
| 22 | +2. :ref:`spec-step-uop` |
| 23 | +3. :ref:`spec-step-macro` |
| 24 | +4. :ref:`spec-step-cache-struct` |
| 25 | +5. :ref:`spec-step-specialize-fn` |
| 26 | +6. :ref:`spec-step-stats` |
| 27 | +7. :ref:`spec-step-opcode-py` |
| 28 | +8. :ref:`spec-step-magic-num` |
| 29 | +9. :ref:`spec-step-regen` |
| 30 | + |
| 31 | + |
| 32 | +.. _spec-step-bytecodes: |
| 33 | + |
| 34 | +1. Modify the Opcode Definition in ``Python/bytecodes.c`` |
| 35 | +--------------------------------------------------------- |
| 36 | + |
| 37 | +In ``Python/bytecodes.c``, locate the existing generic instruction. Change its definition from a top-level instruction (``inst``) to a micro-op (``op``) and prefix its name with an underscore. |
| 38 | + |
| 39 | +For example, convert ``CONTAINS_OP`` into ``_CONTAINS_OP``: |
| 40 | + |
| 41 | +.. code-block:: c |
| 42 | +
|
| 43 | + op(_CONTAINS_OP, (left, right -- res)) { |
| 44 | + // Implementation of generic operation |
| 45 | + int res_val = PySequence_Contains(right, left); |
| 46 | + if (res_val < 0) goto error; |
| 47 | + res = res_val ? Py_True : Py_False; |
| 48 | + } |
| 49 | +
|
| 50 | +
|
| 51 | +.. _spec-step-uop: |
| 52 | + |
| 53 | +2. Add the Specializing Micro-Op (uop) |
| 54 | +-------------------------------------- |
| 55 | + |
| 56 | +Add a new uop definition in ``Python/bytecodes.c`` that triggers the specialization check when the execution counter reaches zero. |
| 57 | + |
| 58 | +.. code-block:: c |
| 59 | +
|
| 60 | + op(_SPECIALIZE_CONTAINS_OP, (left, right -- left, right)) { |
| 61 | + _PySpecializer_Requestation(this_instr, _SPECIALIZE_CONTAINS_OP); |
| 62 | + } |
| 63 | +
|
| 64 | +
|
| 65 | +.. _spec-step-macro: |
| 66 | + |
| 67 | +3. Define the Macro Instruction |
| 68 | +------------------------------- |
| 69 | + |
| 70 | +Combine the specializing uop and the original uop into a macro instruction using ``macro`` syntax in ``Python/bytecodes.c``: |
| 71 | + |
| 72 | +.. code-block:: c |
| 73 | +
|
| 74 | + macro(CONTAINS_OP) = _SPECIALIZE_CONTAINS_OP + _CONTAINS_OP; |
| 75 | +
|
| 76 | +
|
| 77 | +.. _spec-step-cache-struct: |
| 78 | + |
| 79 | +4. Define the Cache Structure in ``Include/internal/pycore_code.h`` |
| 80 | +------------------------------------------------------------------- |
| 81 | + |
| 82 | +Define a C struct for the instruction's inline cache in ``Include/internal/pycore_code.h``. Every cache entry must include at least a 16-bit specialization counter (``counter``), plus any specialized metadata or version pointers needed. |
| 83 | + |
| 84 | +.. code-block:: c |
| 85 | +
|
| 86 | + typedef struct { |
| 87 | + _PySpecializationCacheTop counter; |
| 88 | + // Additional cache fields if needed (e.g., version or type pointers) |
| 89 | + } _PyContainsOpCache; |
| 90 | +
|
| 91 | +
|
| 92 | +.. _spec-step-specialize-fn: |
| 93 | + |
| 94 | +5. Write the Specializing Function in ``Python/specialize.c`` |
| 95 | +------------------------------------------------------------- |
| 96 | + |
| 97 | +Implement the specializing logic in ``Python/specialize.c``. This function inspects the runtime operand types, checks whether they qualify for a fast path, updates the cache, and rewrites the opcode if appropriate. |
| 98 | + |
| 99 | +.. code-block:: c |
| 100 | +
|
| 101 | + void |
| 102 | + _Py_Specialize_ContainsOp(PyObject *left, PyObject *right, _Py_CODEUNIT *instr) |
| 103 | + { |
| 104 | + _PyContainsOpCache *cache = (_PyContainsOpCache *)instr; |
| 105 | + if (PySet_CheckExact(right)) { |
| 106 | + // Specialize for set containment |
| 107 | + instr->op.code = _BINARY_OP_CONTAINS_SET; |
| 108 | + } |
| 109 | + else { |
| 110 | + // Fallback / UNSTATISFIED |
| 111 | + STAT_INC(CONTAINS_OP, failure); |
| 112 | + } |
| 113 | + } |
| 114 | +
|
| 115 | +
|
| 116 | +.. _spec-step-stats: |
| 117 | + |
| 118 | +6. Update Operation Statistics in ``Python/specialize.c`` |
| 119 | +--------------------------------------------------------- |
| 120 | + |
| 121 | +Track specialization hits, misses, and execution counts by calling ``add_stat_dict()`` or incrementing statistic counters in ``Python/specialize.c``: |
| 122 | + |
| 123 | +.. code-block:: c |
| 124 | +
|
| 125 | + STAT_INC(CONTAINS_OP, hit); |
| 126 | +
|
| 127 | +
|
| 128 | +.. _spec-step-opcode-py: |
| 129 | + |
| 130 | +7. Add Cache Layout in ``Lib/opcode.py`` |
| 131 | +---------------------------------------- |
| 132 | + |
| 133 | +Update ``Lib/opcode.py`` to inform Python's ``dis`` (disassembler) module about the size and structure of the new instruction's inline cache entries: |
| 134 | + |
| 135 | +.. code-block:: python |
| 136 | +
|
| 137 | + _specialized_opcodes["CONTAINS_OP"] = { |
| 138 | + "counter": 1, |
| 139 | + } |
| 140 | +
|
| 141 | +
|
| 142 | +.. _spec-step-magic-num: |
| 143 | + |
| 144 | +8. Bump the Magic Number in ``Include/internal/pycore_magic_number.h`` |
| 145 | +---------------------------------------------------------------------- |
| 146 | + |
| 147 | +Because adding or altering inline cache structures changes the bytecode format, increment ``MAGIC_NUMBER`` in ``Include/internal/pycore_magic_number.h``. This ensures older ``.pyc`` files are invalidated and recompiled. |
| 148 | + |
| 149 | + |
| 150 | +.. _spec-step-regen: |
| 151 | + |
| 152 | +9. Regenerate Code Files |
| 153 | +------------------------ |
| 154 | + |
| 155 | +Run the code generators to update auto-generated files (like ``opcode_targets.h``, ``executor_cases.c``, etc.): |
| 156 | + |
| 157 | +On Linux / macOS: |
| 158 | + |
| 159 | +.. code-block:: bash |
| 160 | +
|
| 161 | + make regen-all |
| 162 | +
|
| 163 | +On Windows: |
| 164 | + |
| 165 | +.. code-block:: bash |
| 166 | +
|
| 167 | + build.bat --regen |
| 168 | +
|
| 169 | +
|
| 170 | +See Also |
| 171 | +-------- |
| 172 | + |
| 173 | +* `PEP 659 -- Specializing Adaptive Interpreter <https://peps.python.org/pep-0659/>`_ |
| 174 | +* `CPython PR #116385 <https://github.com/python/cpython/pull/116385>`_ |
| 175 | +* :ref:`dev-workflow` |
0 commit comments