O01, the object header read byte by byte - #131
Merged
Merged
Conversation
The first lesson of the object model part. T08 said every object starts with a reference count and a pointer to its type. That is what almost every article about CPython says and it is not quite what is in memory. The lesson reads the bytes with ctypes out of the reader's own interpreter and checks each field against something Python will tell you another way. On an ordinary sixty four bit build the first word is a thirty two bit ob_refcnt, sixteen bits called ob_overflow that are declared in the struct and referenced nowhere else in the source tree, and sixteen bits of ob_flags. It then works through why an immortal object parks at 3 << 30 rather than at the top of the field, which is so that an extension compiled against 3.11 can increment and decrement without checking and be a billion out either way; the two flag bits that record whether an object was compiled into the binary or promoted while running, and why shutting down has to tell them apart; the identifier strings CPython ships, which is why a literal "self" in your code is immortal and a string CPython has never seen is not; where a length lives for the types that have one, including the assert that keeps Py_SIZE away from integers; and the two words in front of the header that only the cycle collector sees, measured as the gap between __sizeof__ and getsizeof. Every offset is computed from the pointer size rather than hardcoded, so the same cells run in the browser probe, where Python is a thirty two bit build and every number halves. 3.14 and 3.15 print identical output for the whole lesson. Six diagrams as excalidraw and svg. Two glossary terms, PyVarObject and GC pre header, which takes GLOSSARY.md to 108. Both README tables updated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The first lesson of M4, the object model.
T08 already said that every object starts with a reference count and a pointer to its type. That is what almost every article about CPython says, and it is a good enough summary right up until you look at the bytes. On an ordinary sixty four bit build the count is thirty two bits wide, not sixty four, and the rest of that word is doing two other jobs.
So this lesson reads the header out of the reader's own interpreter with
ctypes, and checks every field against something Python will tell you another way. The address comes fromid(), the count is checked againstsys.getrefcount, the type pointer is checked againstid(type(x)), and the length field is checked againstlen().What is in it:
ob_refcntat thirty two bits,ob_overflowat sixteen andob_flagsat sixteen, and the fact thatob_overflowis declared in that struct and appears nowhere else in the entire source tree3 << 30when the line is at2 ** 31and the field ends at2 ** 32, which is so that an extension compiled against 3.11 can increment and decrementNonewithout knowing about immortality and be about a billion out in either direction"self"and"append"in your own code are immortal, because CPython ships a generated table of the identifiers it uses itself and the compiler interns your literal to it, while a string CPython has never heard of is an ordinary heap object with an ordinary count.sys.interndoes not immortalise anything, and the lesson shows that toostrkeeps its length in the same place without being aPyVarObject, and whyPy_SIZEasserts that its argument is not an int and not a boolvalue.__sizeof__()andsys.getsizeof(value)Every offset is computed from
ctypes.sizeof(ctypes.c_void_p)rather than hardcoded as 8 and 16. That is what lets the same cells run in the browser probe, where Python is a thirty two bit build, the header is eight bytes, the pre header gap is eight rather than sixteen, and a static immortal parks at7 << 28. Each of those is written down in a per cell note.3.14 and 3.15 print identical output for every cell in this lesson, which does not happen often and is said in the text.
Six diagrams as
.excalidrawand.svg, all under 1100 px. Ten citations into the pinnedv3.15.0rc1, all resolving. Two glossary terms,PyVarObjectandGC pre header, which takes GLOSSARY.md to 108. Both README tables updated.just checkandjust versionsare clean locally: 29 notebooks run, 940 citations resolve, 310 cells run end to end in the browser probe, and the new cells classify as 1 declared and 5 noted with nothing undeclared and nothing stale.Part of #23.