<div dir="ltr"><div>I have taken PEP 523 for this: <a href="https://github.com/python/peps/blob/master/pep-0523.txt">https://github.com/python/peps/blob/master/pep-0523.txt</a> .<br><br></div>I'm waiting until Guido gets back from vacation, at which point I'll ask for a pronouncement or assignment of a BDFL delegate.<br></div><br><div class="gmail_quote"><div dir="ltr">On Fri, 3 Jun 2016 at 14:37 Brett Cannon <<a href="mailto:brett@python.org">brett@python.org</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="ltr"><div>For those of you who follow python-ideas or were at the PyCon US 2016 language summit, you have already seen/heard about this PEP. For those of you who don't fall into either of those categories, this PEP proposed a frame evaluation API for CPython. The motivating example of this work has been Pyjion, the experimental CPython JIT Dino Viehland and I have been working on in our spare time at Microsoft. The API also works for debugging, though, as already demonstrated by Google having added a very similar API internally for debugging purposes.</div><div><br></div><div>The PEP is pasted in below and also available in rendered form at   <a href="https://github.com/Microsoft/Pyjion/blob/master/pep.rst" target="_blank">https://github.com/Microsoft/Pyjion/blob/master/pep.rst</a> (I will assign myself a PEP # once discussion is finished as it's easier to work in git for this for the rich rendering of the in-progress PEP).<br></div><div><br></div><div>I should mention that the difference from python-ideas and the language summit in the PEP are the listed support from Google's use of a very similar API as well as clarifying the co_extra field on code objects doesn't change their immutability (at least from the view of the PEP).</div><div><br></div>----------<div><div>PEP: NNN</div><div>Title: Adding a frame evaluation API to CPython</div><div>Version: $Revision$</div><div>Last-Modified: $Date$</div><div>Author: Brett Cannon <<a href="mailto:brett@python.org" target="_blank">brett@python.org</a>>,</div><div>        Dino Viehland <<a href="mailto:dinov@microsoft.com" target="_blank">dinov@microsoft.com</a>></div><div>Status: Draft</div><div>Type: Standards Track</div><div>Content-Type: text/x-rst</div><div>Created: 16-May-2016</div><div>Post-History: 16-May-2016</div><div>              03-Jun-2016</div><div><br></div><div><br></div><div>Abstract</div><div>========</div><div><br></div><div>This PEP proposes to expand CPython's C API [#c-api]_ to allow for</div><div>the specification of a per-interpreter function pointer to handle the</div><div>evaluation of frames [#pyeval_evalframeex]_. This proposal also</div><div>suggests adding a new field to code objects [#pycodeobject]_ to store</div><div>arbitrary data for use by the frame evaluation function.</div><div><br></div><div><br></div><div>Rationale</div><div>=========</div><div><br></div><div>One place where flexibility has been lacking in Python is in the direct</div><div>execution of Python code. While CPython's C API [#c-api]_ allows for</div><div>constructing the data going into a frame object and then evaluating it</div><div>via ``PyEval_EvalFrameEx()`` [#pyeval_evalframeex]_, control over the</div><div>execution of Python code comes down to individual objects instead of a</div><div>hollistic control of execution at the frame level.</div><div><br></div><div>While wanting to have influence over frame evaluation may seem a bit</div><div>too low-level, it does open the possibility for things such as a</div><div>method-level JIT to be introduced into CPython without CPython itself</div><div>having to provide one. By allowing external C code to control frame</div><div>evaluation, a JIT can participate in the execution of Python code at</div><div>the key point where evaluation occurs. This then allows for a JIT to</div><div>conditionally recompile Python bytecode to machine code as desired</div><div>while still allowing for executing regular CPython bytecode when</div><div>running the JIT is not desired. This can be accomplished by allowing</div><div>interpreters to specify what function to call to evaluate a frame. And</div><div>by placing the API at the frame evaluation level it allows for a</div><div>complete view of the execution environment of the code for the JIT.</div><div><br></div><div>This ability to specify a frame evaluation function also allows for</div><div>other use-cases beyond just opening CPython up to a JIT. For instance,</div><div>it would not be difficult to implement a tracing or profiling function</div><div>at the call level with this API. While CPython does provide the</div><div>ability to set a tracing or profiling function at the Python level,</div><div>this would be able to match the data collection of the profiler and</div><div>quite possibly be faster for tracing by simply skipping per-line</div><div>tracing support.</div><div><br></div><div>It also opens up the possibility of debugging where the frame</div><div>evaluation function only performs special debugging work when it</div><div>detects it is about to execute a specific code object. In that</div><div>instance the bytecode could be theoretically rewritten in-place to</div><div>inject a breakpoint function call at the proper point for help in</div><div>debugging while not having to do a heavy-handed approach as</div><div>required by ``sys.settrace()``.</div><div><br></div><div>To help facilitate these use-cases, we are also proposing the adding</div><div>of a "scratch space" on code objects via a new field. This will allow</div><div>per-code object data to be stored with the code object itself for easy</div><div>retrieval by the frame evaluation function as necessary. The field</div><div>itself will simply be a ``PyObject *`` type so that any data stored in</div><div>the field will participate in normal object memory management.</div><div><br></div><div><br></div><div>Proposal</div><div>========</div><div><br></div><div>All proposed C API changes below will not be part of the stable ABI.</div><div><br></div><div><br></div><div>Expanding ``PyCodeObject``</div><div>--------------------------</div><div><br></div><div>One field is to be added to the ``PyCodeObject`` struct</div><div>[#pycodeobject]_::</div><div><br></div><div>  typedef struct {</div><div>     ...</div><div>     PyObject *co_extra;  /* "Scratch space" for the code object. */</div><div>  } PyCodeObject;</div><div><br></div><div>The ``co_extra`` will be ``NULL`` by default and will not be used by</div><div>CPython itself. Third-party code is free to use the field as desired.</div><div>Values stored in the field are expected to not be required in order</div><div>for the code object to function, allowing the loss of the data of the</div><div>field to be acceptable (this keeps the code object as immutable from</div><div>a functionality point-of-view; this is slightly contentious and so is</div><div>listed as an open issue in `Is co_extra needed?`_). The field will be</div><div>freed like all other fields on ``PyCodeObject`` during deallocation</div><div>using ``Py_XDECREF()``.</div><div><br></div><div>It is not recommended that multiple users attempt to use the</div><div>``co_extra`` simultaneously. While a dictionary could theoretically be</div><div>set to the field and various users could use a key specific to the</div><div>project, there is still the issue of key collisions as well as</div><div>performance degradation from using a dictionary lookup on every frame</div><div>evaluation. Users are expected to do a type check to make sure that</div><div>the field has not been previously set by someone else.</div><div><br></div><div><br></div><div>Expanding ``PyInterpreterState``</div><div>--------------------------------</div><div><br></div><div>The entrypoint for the frame evalution function is per-interpreter::</div><div><br></div><div>  // Same type signature as PyEval_EvalFrameEx().</div><div>  typedef PyObject* (__stdcall *PyFrameEvalFunction)(PyFrameObject*, int);</div><div><br></div><div>  typedef struct {</div><div>      ...</div><div>      PyFrameEvalFunction eval_frame;</div><div>  } PyInterpreterState;</div><div><br></div><div>By default, the ``eval_frame`` field will be initialized to a function</div><div>pointer that represents what ``PyEval_EvalFrameEx()`` currently is</div><div>(called ``PyEval_EvalFrameDefault()``, discussed later in this PEP).</div><div>Third-party code may then set their own frame evaluation function</div><div>instead to control the execution of Python code. A pointer comparison</div><div>can be used to detect if the field is set to</div><div>``PyEval_EvalFrameDefault()`` and thus has not been mutated yet.</div><div><br></div><div><br></div><div>Changes to ``Python/ceval.c``</div><div>-----------------------------</div><div><br></div><div>``PyEval_EvalFrameEx()`` [#pyeval_evalframeex]_ as it currently stands</div><div>will be renamed to ``PyEval_EvalFrameDefault()``. The new</div><div>``PyEval_EvalFrameEx()`` will then become::</div><div><br></div><div>    PyObject *</div><div>    PyEval_EvalFrameEx(PyFrameObject *frame, int throwflag)</div><div>    {</div><div>        PyThreadState *tstate = PyThreadState_GET();</div><div>        return tstate->interp->eval_frame(frame, throwflag);</div><div>    }</div><div><br></div><div>This allows third-party code to place themselves directly in the path</div><div>of Python code execution while being backwards-compatible with code</div><div>already using the pre-existing C API.</div><div><br></div><div><br></div><div>Updating ``python-gdb.py``</div><div>--------------------------</div><div><br></div><div>The generated ``python-gdb.py`` file used for Python support in GDB</div><div>makes some hard-coded assumptions about ``PyEval_EvalFrameEx()``, e.g.</div><div>the names of local variables. It will need to be updated to work with</div><div>the proposed changes.</div><div><br></div><div><br></div><div>Performance impact</div><div>==================</div><div><br></div><div>As this PEP is proposing an API to add pluggability, performance</div><div>impact is considered only in the case where no third-party code has</div><div>made any changes.</div><div><br></div><div>Several runs of pybench [#pybench]_ consistently showed no performance</div><div>cost from the API change alone.</div><div><br></div><div>A run of the Python benchmark suite [#py-benchmarks]_ showed no</div><div>measurable cost in performance.</div><div><br></div><div>In terms of memory impact, since there are typically not many CPython</div><div>interpreters executing in a single process that means the impact of</div><div>``co_extra`` being added to ``PyCodeObject`` is the only worry.</div><div>According to [#code-object-count]_, a run of the Python test suite</div><div>results in about 72,395 code objects being created. On a 64-bit</div><div>CPU that would result in 579,160 bytes of extra memory being used if</div><div>all code objects were alive at once and had nothing set in their</div><div>``co_extra`` fields.</div><div><br></div><div><br></div><div>Example Usage</div><div>=============</div><div><br></div><div>A JIT for CPython</div><div>-----------------</div><div><br></div><div>Pyjion</div><div>''''''</div><div><br></div><div>The Pyjion project [#pyjion]_ has used this proposed API to implement</div><div>a JIT for CPython using the CoreCLR's JIT [#coreclr]_. Each code</div><div>object has its ``co_extra`` field set to a ``PyjionJittedCode`` object</div><div>which stores four pieces of information:</div><div><br></div><div>1. Execution count</div><div>2. A boolean representing whether a previous attempt to JIT failed</div><div>3. A function pointer to a trampoline (which can be type tracing or not)</div><div>4. A void pointer to any JIT-compiled machine code</div><div><br></div><div>The frame evaluation function has (roughly) the following algorithm::</div><div><br></div><div>    def eval_frame(frame, throw_flag):</div><div>        pyjion_code = frame.code.co_extra</div><div>        if not pyjion_code:</div><div>            frame.code.co_extra = PyjionJittedCode()</div><div>        elif not pyjion_code.jit_failed:</div><div>            if not pyjion_code.jit_code:</div><div>                return pyjion_code.eval(pyjion_code.jit_code, frame)</div><div>            elif pyjion_code.exec_count > 20_000:</div><div>                if jit_compile(frame):</div><div>                    return pyjion_code.eval(pyjion_code.jit_code, frame)</div><div>                else:</div><div>                    pyjion_code.jit_failed = True</div><div>        pyjion_code.exec_count += 1</div><div>        return PyEval_EvalFrameDefault(frame, throw_flag)</div><div><br></div><div>The key point, though, is that all of this work and logic is separate</div><div>from CPython and yet with the proposed API changes it is able to</div><div>provide a JIT that is compliant with Python semantics (as of this</div><div>writing, performance is almost equivalent to CPython without the new</div><div>API). This means there's nothing technically preventing others from</div><div>implementing their own JITs for CPython by utilizing the proposed API.</div><div><br></div><div><br></div><div>Other JITs</div><div>''''''''''</div><div><br></div><div>It should be mentioned that the Pyston team was consulted on an</div><div>earlier version of this PEP that was more JIT-specific and they were</div><div>not interested in utilizing the changes proposed because they want</div><div>control over memory layout they had no interest in directly supporting</div><div>CPython itself. An informal discusion with a developer on the PyPy</div><div>team led to a similar comment.</div><div><br></div><div>Numba [#numba]_, on the other hand, suggested that they would be</div><div>interested in the proposed change in a post-1.0 future for</div><div>themselves [#numba-interest]_.</div><div><br></div><div>The experimental Coconut JIT [#coconut]_ could have benefitted from</div><div>this PEP. In private conversations with Coconut's creator we were told</div><div>that our API was probably superior to the one they developed for</div><div>Coconut to add JIT support to CPython.</div><div><br></div><div><br></div><div>Debugging</div><div>---------</div><div><br></div><div>In conversations with the Python Tools for Visual Studio team (PTVS)</div><div>[#ptvs]_, they thought they would find these API changes useful for</div><div>implementing more performant debugging. As mentioned in the Rationale_</div><div>section, this API would allow for switching on debugging functionality</div><div>only in frames where it is needed. This could allow for either</div><div>skipping information that ``sys.settrace()`` normally provides and</div><div>even go as far as to dynamically rewrite bytecode prior to execution</div><div>to inject e.g. breakpoints in the bytecode.</div><div><br></div><div>It also turns out that Google has provided a very similar API</div><div>internally for years. It has been used for performant debugging</div><div>purposes.</div><div><br></div><div><br></div><div>Implementation</div><div>==============</div><div><br></div><div>A set of patches implementing the proposed API is available through</div><div>the Pyjion project [#pyjion]_. In its current form it has more</div><div>changes to CPython than just this proposed API, but that is for ease</div><div>of development instead of strict requirements to accomplish its goals.</div><div><br></div><div><br></div><div>Open Issues</div><div>===========</div><div><br></div><div>Allow ``eval_frame`` to be ``NULL``</div><div>-----------------------------------</div><div><br></div><div>Currently the frame evaluation function is expected to always be set.</div><div>It could very easily simply default to ``NULL`` instead which would</div><div>signal to use ``PyEval_EvalFrameDefault()``. The current proposal of</div><div>not special-casing the field seemed the most straight-forward, but it</div><div>does require that the field not accidentally be cleared, else a crash</div><div>may occur.</div><div><br></div><div><br></div><div>Is co_extra needed?</div><div>-------------------</div><div><br></div><div>While discussing this PEP at PyCon US 2016, some core developers</div><div>expressed their worry of the ``co_extra`` field making code objects</div><div>mutable. The thinking seemed to be that having a field that was</div><div>mutated after the creation of the code object made the object seem</div><div>mutable, even though no other aspect of code objects changed.</div><div><br></div><div>The view of this PEP is that the `co_extra` field doesn't change the</div><div>fact that code objects are immutable. The field is specified in this</div><div>PEP as to not contain information required to make the code object</div><div>usable, making it more of a caching field. It could be viewed as</div><div>similar to the UTF-8 cache that string objects have internally;</div><div>strings are still considered immutable even though they have a field</div><div>that is conditionally set.</div><div><br></div><div>The field is also not strictly necessary. While the field greatly</div><div>simplifies attaching extra information to code objects, other options</div><div>such as keeping a mapping of code object memory addresses to what</div><div>would have been kept in ``co_extra`` or perhaps using a weak reference</div><div>of the data on the code object and then iterating through the weak</div><div>references until the attached data is found is possible. But obviously</div><div>all of these solutions are not as simple or performant as adding the</div><div>``co_extra`` field.</div><div><br></div><div><br></div><div>Rejected Ideas</div><div>==============</div><div><br></div><div>A JIT-specific C API</div><div>--------------------</div><div><br></div><div>Originally this PEP was going to propose a much larger API change</div><div>which was more JIT-specific. After soliciting feedback from the Numba</div><div>team [#numba]_, though, it became clear that the API was unnecessarily</div><div>large. The realization was made that all that was truly needed was the</div><div>opportunity to provide a trampoline function to handle execution of</div><div>Python code that had been JIT-compiled and a way to attach that</div><div>compiled machine code along with other critical data to the</div><div>corresponding Python code object. Once it was shown that there was no</div><div>loss in functionality or in performance while minimizing the API</div><div>changes required, the proposal was changed to its current form.</div><div><br></div><div><br></div><div>References</div><div>==========</div><div><br></div><div>.. [#pyjion] Pyjion project</div><div>   (<a href="https://github.com/microsoft/pyjion" target="_blank">https://github.com/microsoft/pyjion</a>)</div><div><br></div><div>.. [#c-api] CPython's C API</div><div>   (<a href="https://docs.python.org/3/c-api/index.html" target="_blank">https://docs.python.org/3/c-api/index.html</a>)</div><div><br></div><div>.. [#pycodeobject] ``PyCodeObject``</div><div>   (<a href="https://docs.python.org/3/c-api/code.html#c.PyCodeObject" target="_blank">https://docs.python.org/3/c-api/code.html#c.PyCodeObject</a>)</div><div><br></div><div>.. [#coreclr] .NET Core Runtime (CoreCLR)</div><div>   (<a href="https://github.com/dotnet/coreclr" target="_blank">https://github.com/dotnet/coreclr</a>)</div><div><br></div><div>.. [#pyeval_evalframeex] ``PyEval_EvalFrameEx()``</div><div>   (<a href="https://docs.python.org/3/c-api/veryhigh.html?highlight=pyframeobject#c.PyEval_EvalFrameEx" target="_blank">https://docs.python.org/3/c-api/veryhigh.html?highlight=pyframeobject#c.PyEval_EvalFrameEx</a>)</div><div><br></div><div>.. [#pycodeobject] ``PyCodeObject``</div><div>   (<a href="https://docs.python.org/3/c-api/code.html#c.PyCodeObject" target="_blank">https://docs.python.org/3/c-api/code.html#c.PyCodeObject</a>)</div><div><br></div><div>.. [#numba] Numba</div><div>   (<a href="http://numba.pydata.org/" target="_blank">http://numba.pydata.org/</a>)</div><div><br></div><div>.. [#numba-interest]  numba-users mailing list:</div><div>   "Would the C API for a JIT entrypoint being proposed by Pyjion help out Numba?"</div><div>   (<a href="https://groups.google.com/a/continuum.io/forum/#!topic/numba-users/yRl_0t8-m1g" target="_blank">https://groups.google.com/a/continuum.io/forum/#!topic/numba-users/yRl_0t8-m1g</a>)</div><div><br></div><div>.. [#code-object-count] [Python-Dev] Opcode cache in ceval loop</div><div>   (<a href="https://mail.python.org/pipermail/python-dev/2016-February/143025.html" target="_blank">https://mail.python.org/pipermail/python-dev/2016-February/143025.html</a>)</div><div><br></div><div>.. [#py-benchmarks] Python benchmark suite</div><div>   (<a href="https://hg.python.org/benchmarks" target="_blank">https://hg.python.org/benchmarks</a>)</div><div><br></div><div>.. [#pyston] Pyston</div><div>   (<a href="http://pyston.org" target="_blank">http://pyston.org</a>)</div><div><br></div><div>.. [#pypy] PyPy</div><div>   (<a href="http://pypy.org/" target="_blank">http://pypy.org/</a>)</div><div><br></div><div>.. [#ptvs] Python Tools for Visual Studio</div><div>   (<a href="http://microsoft.github.io/PTVS/" target="_blank">http://microsoft.github.io/PTVS/</a>)</div><div><br></div><div>.. [#coconut] Coconut</div><div>   (<a href="https://github.com/davidmalcolm/coconut" target="_blank">https://github.com/davidmalcolm/coconut</a>)</div><div><br></div><div><br></div><div>Copyright</div><div>=========</div><div><br></div><div>This document has been placed in the public domain.</div><div><br></div><div><br></div><div> </div><div>..</div><div>   Local Variables:</div><div>   mode: indented-text</div><div>   indent-tabs-mode: nil</div><div>   sentence-end-double-space: t</div><div>   fill-column: 70</div><div>   coding: utf-8</div><div>   End:</div></div><div><br></div></div></blockquote></div>