gh-129724: Make marshalled code output deterministic - #156862
Draft
khmyznikov wants to merge 5 commits into
Draft
Conversation
Reuse the manually constructed ambiguous constants across refleak repetitions so free-threaded constant immortalization is not counted as a marshal leak. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ad09eb5a-b18e-4bb2-9225-d2d3d589452e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ad09eb5a-b18e-4bb2-9225-d2d3d589452e
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
4 tasks
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.
Summary
This makes marshalled code-object output deterministic in free-threaded builds, including when multiple threads compile modules concurrently.
Fixes #129724.
Fixes #156504.
The standard build retains current
maincompiler-cache and marshal behavior.Problem
Marshal decides whether to add an object to its reference table using
_PyObject_IsUniquelyReferenced(). In a free-threaded build, that result can depend on object ownership and shared-reference state. The same code object can therefore receive differentFLAG_REFplacements depending on thread timing and prior accesses.Because a
.pycfile contains the marshalled code object after its 16-byte header, this makes otherwise identical parallel compilations produce byte-different.pycfiles. Running a free-threaded interpreter with-X gil=1does not avoid the problem because it does not change the free-threaded object's reference representation.Approach
For a top-level code object in a free-threaded build, marshal now uses deterministic reference tracking:
In free-threaded builds, the compiler also avoids sharing rebuildable constant containers across code units. Frozensets and slices use a code-unit-local cache instead of the compiler-wide cache. This preserves same-code-unit deduplication while preventing compilation order from changing the object graph later passed to marshal.
Valid source can create distinct NaN constants whose marshal encodings compare equal. In free-threaded builds, constant folding therefore leaves NaN-containing set displays as runtime
BUILD_SEToperations instead of creating an ambiguously ordered constant.Marshal versions 0 through 2 do not have a reference table, so their reference-tracking behavior is unchanged. The new ambiguous-set rejection applies to every marshal version when deterministic code-object mode is active. Generic non-code-object marshaling retains the existing path.
Compatibility
mainsharing behavior. Baseline and candidate bytes matched for both tested compilation histories.maincan itself produce different standard-build bytes after an unrelated compilation changes frozenset sharing. This pre-existing behavior is outside the free-threaded scope of this change..pycsize increased by 77 bytes forpip across 17 of 404 files and by 63 bytes for Plotly across 4 of 1,507
files. The largest per-file change was 23 bytes, and each total change was
below 0.002%.
Determinism results
Testing used x64 Release standard and free-threaded interpreters built from current
main(42de93a62e) and the core implementation (eae9b877a7) on the same 8-vCPU Windows VM. The final compatibility follow-up is3f056a9782; it narrows standard-build compiler behavior without changing the free-threaded marshal algorithm.mainmarshal.dumps(code)py_compile-X gil=1marshal.dumps(code)-X gil=1py_compileThat is 80 candidate stress attempts with no differing output.
The exact final tree also completed 10 attempts for each combination locally: 40 additional attempts with no differing output.
Performance
The same 8-vCPU VM ran eight paired current-main/candidate rounds, alternating process order. Ratios below are the median paired candidate/baseline ratio; values above 1 are slower.
mainmarshal.dumps(code)marshal.loads(code_payload)marshal.loadsThe stable tradeoff is the synthetic repeated free-threaded
marshal.dumps(code)case: about 51% more time and 35% more peak memory because deterministic mode records all supported references.marshal.loadsis about 5% slower with 4.5% more peak memory. Compile-plus-marshal was neutral, and the synthetic cold-import result was about 1.4% slower. Generic and threaded measurements had wider VM variance and should not be interpreted as performance improvements.Integrated pip workload
The change was tested end-to-end with pip PR 14286, which parallelizes Windows wheel bytecode compilation. The benchmark copy removed only pip's guard that keeps free-threaded builds serial. Both CPython variants and both GIL modes used eight worker threads.
Each timing is the median of six balanced runs on the same eight-vCPU Windows VM with Defender antivirus, real-time, IOAV, and behavior monitoring enabled.
The benchmark fixed
SOURCE_DATE_EPOCH=1704067200andPYTHONHASHSEED=0toisolate thread-order effects.
Current main produced serial/parallel artifact differences in 14 of 48
full-matrix parallel installs. The deterministic build produced zero
differences in all 48.
The exact-final follow-up used four counterbalanced runs per wheel, filesystem,
and GIL mode. Current main differed in 9 of 32 parallel installs; exact
3f056a9782differed in 0 of 32, and its 32 serial repeats were identical.Together, the full and exact-final matrices successfully unmarshalled 305,760
measured
.pycpayloads.Across the six-run matrix, aggregate serial timing for the fixed runtime was
about 1% slower than current main and within run-to-run variance, with no
consistent parallel penalty. In the exact-final matrix,
candidate/current-main peak committed/pagefile ratios ranged from 0.998-1.004
serial and 0.960-1.020 parallel, so there was no consistent whole-process
memory increase attributable to this change. Absolute fixed-runtime peak
committed usage was 133.5-134.8 MiB serial and 239.6-300.6 MiB parallel; peak
working set was 98.7-107.5 MiB serial and 169.3-196.5 MiB parallel. These
whole-process measurements do not replace the marshal-only peak-memory result.
Default GIL-off PythonT retained a 1.44x-3.64x speedup. With the GIL re-enabled, NTFS still benefited substantially, while the trusted Dev Drive was neutral to slightly slower.
The pip implementation used eight workers on a VM with eight logical CPUs;
these measurements do not establish the optimal worker count elsewhere.
WSL cross-platform confirmation
The exact candidate was also built with GCC 15.2 on x86-64 Ubuntu 26.04 WSL2
and tested on native ext4 storage. The CPython diff has no OS- or
CPU-architecture-specific branch; its conditional behavior uses only
Py_GIL_DISABLED. This run therefore adds Linux ABI/toolchain coverage, butnot Arm64 coverage.
The standalone reproducer found a current-main difference on attempt 1 for
both
marshalandpy_compile, with the GIL both disabled and enabled. Exact3f056a9782completed all 40 corresponding attempts with no difference.Focused
test_marshal,test_compile, andtest_coderuns passed 313 testsin each GIL mode.
For an integrated check, the pip PR copy was temporarily enabled on Linux and
free-threaded builds. Six counterbalanced runs used four workers:
Current main differed from serial output in 13 of 24 parallel installs. The
candidate differed in 0 of 24, and all 91,728 measured
.pycpayloadsunmarshalled successfully. Candidate/current-main geometric-mean timing ratios
were 0.993 serial and 1.018 parallel. The approximately 1.8% aggregate
parallel difference varied by workload and is small relative to the GIL-off
pip gain. Peak virtual size was effectively identical; geometric-mean peak
RSS ratios were 1.001 serial and 1.014 parallel.
The size effect matched Windows: pip
.pycfiles grew by 77 total bytes andPlotly by 63, with a maximum per-file change of 23 bytes. The result also
confirms the pip policy distinction: default GIL-off PythonT benefits on ext4,
while GIL-on PythonT was 21-29% slower with four workers.
Alternatives measured
The set-sort ordinal is normally a cached small integer, so replacing it with a temporary set or dict would add hashing and table allocation to save one tuple slot. That was not retained as a promising optimization.
Validation
Tools/patchcheck/patchcheck.pypassed.-X gil=1focused suite: 1,812 tests successful.test_marshal,test_compile, andtest_codepassed in both standard and free-threaded builds with-R 3:3.One
test_device_encodingassertion was excluded from the final clean host run because the automation pseudo-console reports stdin as a TTY whileos.device_encoding(0)isNone. Currentmainfails the same isolated assertion in that environment.AI-assisted development disclosure
GitHub Copilot was used to assist investigation, implementation, test drafting, and benchmark organization.