[Python-Dev] About the new CFrame structure

106 views
Skip to first unread message

Gabriele

unread,
Dec 20, 2021, 1:22:56 PM12/20/21
to Python Dev
Hi there

I hope you would indulge me in asking for some details about the new
CFrame structure, even in the form of existing literature (e.g. PEP)
where the idea behind it is explained.

Also, I'd like to a quick question, if I may. There now appear to be
two ways of unwinding the frame stack: either iterate over
CFrame.previous, or the more traditional PyFrameObject.f_back. I
suspect there are reasons why these are perhaps not actually
equivalent, and indeed this is mainly what I'd like to read in the
literature I've requested above.

Cheers,
Gabriele

--
"Egli è scritto in lingua matematica, e i caratteri son triangoli,
cerchi, ed altre figure
geometriche, senza i quali mezzi è impossibile a intenderne umanamente parola;
senza questi è un aggirarsi vanamente per un oscuro laberinto."

-- G. Galilei, Il saggiatore.
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/KQOQTLR5IXMJXYZGPDHWR32I2Z53UVBL/
Code of Conduct: http://python.org/psf/codeofconduct/

Brandt Bucher

unread,
Dec 20, 2021, 2:21:22 PM12/20/21
to pytho...@python.org
Hi Gabriele!

> I hope you would indulge me in asking for some details about the new CFrame structure, even in the form of existing literature (e.g. PEP) where the idea behind it is explained.

There isn't too much documentation on this, unfortunately (since these are all very unstable, low-level interpreter details), but a good place to start would be https://bugs.python.org/issue46090.

Based on my own understanding (from reading the source code):
- There are three relevant structures: CFrame, InterpreterFrame, and PyFrameObject.
- PyFrameObjects are just PyObject wrappers around an InterpreterFrame, where all of the *actual* frame state for the Python stack is maintained.
- They are created lazily (for example, when sys._getframe() is called).
- See https://github.com/python/cpython/pull/27077.
- InterpreterFrames live in a "datastack" for fast allocation and deallocation.
- This "datastack" lives on the PyThreadState.
- Because of how it is designed, InterpreterFrames must be allocated/deallocated "in order".
- If an InterpreterFrame is cleared, but still has a live PyFrameObject that points to it, it will copy itself *into* the PyFrameObject first (to guarantee that the PyFrameObject keeps working).
- See https://github.com/python/cpython/pull/26076.
- A single CFrame is statically allocated inside of each _PyEval_EvalFrameDefault call, so it corresponds to the C stack, not the Python stack.
- It links to a chain of one or more InterpreterFrames.
- Multiple InterpreterFrames can correspond to a single CFrame!
- This is a performance optimization in 3.11: rather than enter a new call to _PyEval_EvalFrameDefault, calls to pure-Python code just create a new InterpreterFrame, set it as the current one, and continue execution.
- You can see how many InterpreterFrames correspond to the current CFrame by reading the "depth" member of the current InterpreterFrame.
- A value of 0 indicates that this is the only InterpreterFrame for this CFrame.
- A value of 42 means that this optimization has been performed 42 times (and there are currently 43 InterpreterFrames executing in this CFrame).

> Also, I'd like to a quick question, if I may. There now appear to be two ways of unwinding the frame stack: either iterate over CFrame.previous, or the more traditional PyFrameObject.f_back. I suspect there are reasons why these are perhaps not actually equivalent, and indeed this is mainly what I'd like to read in the literature I've requested above.

The above outline probably makes the differences clear:
- PyFrameObject.f_back just gives you a dummy wrapper around the previous frame object.
- It's not really useful for unwinding anything.
- InterpreterFrame.previous gives you the previous interpreter frame (duh!).
- This is probably what you want.
- CFrame.previous gives you the previous call to _PyEval_EvalFrameDefault.
- It's not really useful for unwinding anything.
- This is only really useful to maintain the current tracing state when returning.

Hopefully this helps! Somebody (Pablo or Mark?) will probably jump in here if I got anything wrong.

Brandt
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/GDCPNESE2BWJUNPYFANCZVZK4EZNTKAF/

Brandt Bucher

unread,
Dec 20, 2021, 2:46:33 PM12/20/21
to pytho...@python.org
Just to clear up a quick point I made:

> - PyFrameObject.f_back just gives you a dummy wrapper around the previous frame object.
> - It's not really useful for unwinding anything.

That should read "previous InterpreterFrame", rather than "previous frame object".

Also, everything I wrote above is in the context of 3.11. InterpreterFrames don't exist in 3.10 and below, so in those versions PyFrameObject.f_back is indeed what you probably want.
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/TKPWWP33QJJEVEIP63C4SIEMVBY44LCW/

Guido van Rossum

unread,
Dec 20, 2021, 2:48:12 PM12/20/21
to Gabriele, Python Dev
Hi Gabriele,

I think the code is currently the only documentation, since this is considered a CPython internal issue. I'm CC'ing Mark Shannon, since he designed this for 3.11.

For a bit of background, see this issue in the Faster CPython tracker: https://github.com/faster-cpython/ideas/issues/31 -- maybe this will help you understand the background (though what was implemented doesn't match the discussion there precisely).

Since you seem to be the author of Austin, a sampling profiler for Python code (which I presume itself is written in C or C++), I'm guessing you're asking because you're looking to make Austin work with 3.11. (Actually, the CFrame structure appears in 3.10, but it's not usable for unwinding stack frames in 3.10, since the 3.10 version doesn't have a pointer to the interpreter frame -- it's only used to speed up access to the `use-tracing` flag. So I'm assuming you're asking about 3.10.)

One of the goals for the Faster CPython project that has only become in focus for the team recently is helping tools like Austin (and others like Cython, PyDev and Greenlets) that use internal interpreter state figure out how to get their code working in 3.11, without making too many commitments to stable APIs -- we're already thinking about more performance improvements in 3.12 and beyond, which most likely will require us to change the internal data structures again. (We do commit to leaving these internal data structures unchanged for bugfix release cycles, so that once you've got it working for 3.11.0, it should work without changes for 3.11.1, 3.11.2, etc. In fact we hope to stabilize everything starting with the release of 3.11b1, May 2020.)

Oh, I see that Brandt has already given a better overview of how this stuff actually works (he's a faster typist than me :-), so I'll just sign off here. If you have more specific questions, you can continue this thread, or you can create a new Discussion item in the https://github.com/faster-cpython/ideas repo.

--Guido


--
--Guido van Rossum (python.org/~guido)

Pablo Galindo Salgado

unread,
Dec 20, 2021, 3:22:44 PM12/20/21
to Gabriele, Python Dev
Hi Gabriele,

In addition to what Guido and Brandt have already said, I can help to you adapting Austin to 3.11 as I reviewed or authored some of these changes and I have already been helping some projects do the relevant changes as well as in my own tools.

What you want to do si the following for unwinding:

* Go from _PyRuntime -> PyThreadState -> CFrame -> current_frame

This will lead you to s PyInterpreterFrame that you should use for unwinding the entire thread stack. The difference is that cframe->previous will skip you several frames as it points to the previous CFrame, but there are a one to many relationships between CFrame and interpreter frames because several python functions can now reuse the same evaluation loop.

Also, I would recommend waiting until beta freeze to start adapting anything as things can still massively change until then for 3.11.

If you have any questions or you need help, feel free to ping me in GitHub if you want.

Regards from rainy London,
Pablo Galindo Salgado


Gabriele

unread,
Dec 20, 2021, 4:38:53 PM12/20/21
to Python Dev
Brandt, Guido, Pablo

Many thanks for your helpful answers. Indeed I'm asking because I just finished working on some improvements to Austin and got back to looking into what was coming in order to add support for 3.11 (plus make use of some of the changes that I recently contributed, like PyThreadState.native_thread_id, Py_Version and code.co_qualname). Pablo's suggestion of waiting until 3.11 becomes more stable is a sensible one, but in the meantime I wanted to understand where the frame stack management is heading to at least have an idea of what's in store for Austin :). I'm very much satisfied by all the details in your replies as they answer all my questions, for now, so thanks a lot for that.

A final thought: I have discussed some of the technical details of the recent improvements to Austin in a blog post (https://p403n1x87.github.io/increasing-austin-accuracy-with-a-dobule-heap-trick.html). My experiments seem to suggest that, the less sparse the frame objects are in memory, the more accurate tools like Austin can be. So this makes me wonder if it would make sense for CPython to ensure frame objects are created in a contiguous block of memory (perhaps there could be benefits from the locality of reference, although it's not obvious to me why this would be the case at the moment).

Best,
Gabriele

Pablo Galindo Salgado

unread,
Dec 20, 2021, 5:08:43 PM12/20/21
to Gabriele, Python Dev
Hi Gabriele,

>> So this makes me wonder if it would make sense for CPython to ensure frame objects are created in a contiguous block of memory (perhaps there could be benefits from the locality of reference, although it's not obvious to me why this would be the case at the moment).


That's already the case for 3.11: we currently allocate frames in a continuous, per thread stack (it includes some of the frame memory and arrays apart from the frames themselves).

Check this for more details:

Also, don't rely on this on any way or form as this code can and will likely change a lot (even between patch versions if we found bugs).

Regards from rainy London,
Pablo Galindo Salgado



Gabriele Tornetta

unread,
Dec 20, 2021, 5:12:20 PM12/20/21
to pytho...@python.org
> That's already the case for 3.11

Ah, that's awesome news! Like with the rest, I'll wait and see what shape this ends up taking :).

Cheers,
Gab
_______________________________________________
Python-Dev mailing list -- pytho...@python.org
To unsubscribe send an email to python-d...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at https://mail.python.org/archives/list/pytho...@python.org/message/7VJRYSKXWCQ67EUFJRKPJKEPZ7JB2A7F/
Reply all
Reply to author
Forward
0 new messages