Repository navigation
gh-129724: gh-156504: Make marshal output independent of reference counts - #159192
Conversation
Documentation build overview
12 files changed ·
|
|
🤖 New build scheduled with the buildbot fleet by @gpshead for commit 355cb8c 🤖 Results will be shown at: https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F159192%2Fmerge If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again. |
The marshal writer decided whether an object can be referenced later (FLAG_REF) from its reference count, so the output for the same value depended on what else in the process held a reference to its parts, and differed between the default and the free-threaded builds' view of the same objects. This is the remaining cause of pythongh-129724 and the cause of pythongh-156504. FLAG_REF is now set exactly on the objects which are written more than once in the dump. The writer still makes one pass: it records the position of the type byte of each object it remembers and of each TYPE_REF, and when the dump is complete it sets the flags and renumbers the references. A reference count of one is only used to skip the hashtable. Interned strings are no longer flagged unconditionally. The reader is unchanged and the format version is not bumped: both the old and the new output are loaded by old and new readers. PyMarshal_WriteObjectToFile() now builds the data in memory and writes it with one fwrite(), since the flags are only known at the end. Nothing is written if the object cannot be marshalled.
355cb8c to
7893950
Compare
|
🤖 New build scheduled with the buildbot fleet by @gpshead for commit 7893950 🤖 Results will be shown at: https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F159192%2Fmerge If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again. |
|
Could you reduce the number of NEWS entries to one or two? In What's New, the C API and library changes need to be documented separately because they are in different sections, but I think related changes can be combined into a single NEWS entry. |
|
ETOOMUCHNEWS - good call, done. |
|
GH-159290 is a backport of this pull request to the 3.15 branch. |
marshal.dumps()could give different bytes for the same value, because the writer used the reference count of each object to decide whether a later occurrence may refer back to it (FLAG_REF). The count depends on what else in the process holds a reference to the object, and on the free-threaded build on which thread owns it. This is the cause of gh-156504 and one of the two causes of gh-129724 (the other is in the compiler: see the companion PR gh-159193).FLAG_REFis now set on exactly the objects that are written more than once in the dump.Example
With this PR both calls return the first form. Where an object really is written twice, the index of the reference no longer counts objects that were flagged for nothing:
How
The reader numbers the flagged objects in the order it meets them, so the writer cannot know the index of a reference until it knows which earlier objects end up flagged. It still makes a single pass over the objects, and fixes up the output at the end.
flowchart TB subgraph before["main"] direction TB b1["w_ref(obj)"] --> b2{"refcount == 1?"} b2 -- yes --> b3["write object<br>no flag"] b2 -- no --> b4{"seen before?"} b4 -- no --> b5["write object + FLAG_REF<br>index = number flagged so far"] b4 -- yes --> b6["write TYPE_REF index"] end subgraph after["this PR"] direction TB a1["w_ref(obj)"] --> a2{"refcount == 1?"} a2 -- yes --> a3["write object<br>(cannot occur twice)"] a2 -- no --> a4{"seen before?"} a4 -- no --> a5["write object, no flag yet<br>remember offset of its type byte"] a4 -- yes --> a6["mark object shared<br>write TYPE_REF + provisional index<br>remember its position"] a5 --> a7 a6 --> a7["w_resolve_refs(), once, at the end:<br>set FLAG_REF on shared objects,<br>number them, rewrite TYPE_REF indexes"] endPyMarshal_WriteObjectToFile()now builds the data in memory and writes it with onefwrite(), since the flags are only known at the end. Nothing is written if the object cannot be marshalled. Documented inDoc/c-api/marshal.rst.Compatibility
The bytes of every
.pycchange (fewer flags), which is whyPrograms/test_frozenmain.his regenerated.marshal.loads()gives the same objects with the same sharing.The generated pycs can be loaded by prior Python versions, but this kind of change might be considered disruptive for some when considering a backport. Realistically: prior versions are all "broken" by not producing stable output in real world scenarios, so any bug fixes going in that stabilize how things work are going to lead to some newly generated bytecode changing vs what was seen in the past, as already happens between runs compiling the same source file under different process state today.
Related: #159197 is stacked on top of this.
Performance
Release builds, not PGO, compared with main:
dumpsof the code objects of the stdlibdumpsdominated by references (millions ofTYPE_REF)dumpsof unshared data (1M distinct floats)loadsof the new outputWriter memory grows by 8 bytes per object with a reference count above 1.
Tests
ReferenceTestCaseintest_marshalchecks exact bytes: single occurrences are not flagged whatever their reference count, interned strings, references at every buffer offset, objects seen before and after the ones they share with, NaNs in sets.test_capi.test_marshalchecks that nothing is written on error.drafted using Claude, reviewed & iterated upon by me.
Fixes #156504