Repository navigation
Conversation
…e object The compiler merges equal constants of a module through its constant cache. intern_constants() then ran on each code object and rebuilt a frozenset (or, in the free-threaded build, a slice) when one of its strings was replaced by an already interned one, so a constant shared by several functions was copied for some of them depending on which strings happened to be interned in the process. The class body's __qualname__ constant and co_qualname had the same problem. marshal then wrote different bytes for the same source. Intern the eligible strings of the final constants of each code unit after optimization and before the code object is created. Tuples are updated in place; a frozenset or slice is rebuilt once and stored in the constant cache so that later units get the same object. The same code is used in both builds. Which strings are interned does not change, except that identifier-like strings in slice constants are now interned in the default build too. TestConstantsIdentity compares the identity graph and the marshal output of code compiled in differently prepared processes, including names tuples shared with constants.
In the free-threaded build, compile() of a source string sets suppress_co_const_immortalization so that its output is the same as in the default build. The branch for AST objects returned before that, so constants were replaced by the immortal ones of the interpreter's table while a tuple of names (such as co_names) which was the same object was not, and the marshal output depended on the code objects created before in the process.
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.
Note
Draft: stacked on #159193. This branch contains that PR's commit; only the last commit is new. The code change itself is independent, but its tests extend
TestConstantsIdentity, which #159193 adds. It stays a draft until #159193 is merged.On the free-threaded build, the
marshaloutput for code compiled from an AST depended on which code objects had been created earlier in the process:len(d_src)len(d_ast)The default build gives 230 in every case, and so does the free-threaded build with this PR.
Cause and change
The compiler makes a tuple of names (
co_names,co_localsplusnames) the same object as an equal constant: keyword names, afromlist,__static_attributes__. On the free-threaded build,intern_constants()replaces each constant by the equal immortal object of a per-interpreter table, if there is one, but does not replace the tuple of names.compile()of a source string turns that off withtstate->suppress_co_const_immortalization(gh-118527). The branch that handles AST objects returned before the flag was set.flowchart TB subgraph before["main, free-threaded"] direction TB b0["compiler: constant ('a', 'b')<br>is the same tuple as co_names"] b0 -- "compile(ast)" --> b2["immortalization<br>not suppressed"] b0 -- "compile(str)" --> b1["immortalization<br>suppressed"] b2 --> b4{"equal tuple already<br>in the immortal table?"} b4 -- yes --> b6["constant replaced, co_names not:<br>two tuples, marshal writes both"] b4 -- no --> b5["still one tuple"] b1 --> b3["still one tuple<br>marshal: object + reference"] end subgraph after["this PR"] direction TB a0["compiler: constant ('a', 'b')<br>is the same tuple as co_names"] a0 -- "compile(ast)" --> a1["immortalization<br>suppressed"] a0 -- "compile(str)" --> a1 a1 --> a3["still one tuple<br>marshal: object + reference"] endThe change is the same increment/decrement pair around
_PyAST_Compile()inbuiltin_compile_impl()that already surrounds_Py_CompileString().Effects
.pyc(assertion rewriters,SourceLoader.source_to_code()given an AST) get reproducible output. Compiling the standard library from ASTs in one process, 179 of 2067 files gave different bytes depending on file order; now none do.
compile(ast)no longer has immortal constants, as is already the case forcompile(str). It gets them when it is later loaded from a.pyc. Tools that compile many ASTs stop growing the immortal table: for 10000 distinct functions, +141286 memory blocks on main, +606 with this PR.Not covered
exec()of a string,Py_CompileString*(),PyRun_*()andmarshal.loads()do not set the flag and keep the same dependence on history. None of them is on a path that writes.pycfiles.Tests
Every
TestConstantsIdentitycase is now also compiled from an AST.test_constants_not_immortal(free-threaded only) checks that a tuple constant fromcompile()of a string or an AST is not immortal even after an equal one entered the table.marshaloutput ofcompile()of an AST depends on what was compiled before #159188