Add R04, frozen modules - #182
Merged
Merged
Conversation
The fourth runtime lesson. R03 ended on the fact that import os never opens os.py, because FrozenImporter gets asked before PathFinder, so this one goes after the modules that live inside the binary. Ten code cells, twenty one citations, six diagrams, four glossary terms and two Tier 1 recordings. The spine is the chicken and egg problem. The import system is written in Python, in Lib/importlib/_bootstrap.py, so it cannot be imported. CPython cuts the loop by compiling that file during its own build, marshalling the code object and writing the bytes into the binary as a C array. The proof that this works is in the bytecode: the module body of _frozen_importlib contains zero IMPORT_NAME opcodes, while _frozen_importlib_external, which is loaded second and by then has an import system, contains eight. init_importlib hands sys and _imp in as arguments, which is why _bootstrap.py never writes import sys. A stock 3.15 build freezes thirty three names in three arrays that the flag treats differently. Three are the import system and no setting can remove them, because look_up_frozen walks that array unconditionally and only the other two sit behind a check. Nineteen are what a bare startup needs. Eleven are hello world modules for the test suite. A frozen module still knows where it came from. FrozenImporter.find_spec works the source path out from sys._stdlib_dir and parks it on the spec as loader_state, and the loader copies it onto __file__, so the spec says frozen and __file__ says a real path and both are true. That is what makes a traceback through frozen code readable: linecache sees a filename starting with <frozen and reads __file__ out of the globals it was handed instead. The cost story is not the one you would guess. Both paths end in the same marshal.loads over the same bytes, and that call is most of the bill. What freezing removes is the finder search and the file read in front of it, which the timing cell separates into four steps rather than comparing two totals. The first version of that cell claimed frozen loading cost half as much, which was measurement noise and did not reproduce under a real kernel. The two recordings run the same program on a release build and a debug build, and the interesting part is that the builds disagree about the default. initconfig.c turns frozen modules off under Py_DEBUG, so the program was rewritten to ask for on and off explicitly and report the default as a fact. Freezing gives back 14.2 percent of a release startup and 9.9 percent of a debug one. Both measurements alternate the two cases round by round, because running one case forty times and then the other measures the page cache. New glossary terms: frozen module, import bootstrap, loader state and module alias. GLOSSARY.md is 226 terms, CLAIMS.md is 589 claims across 75 lessons, and citations.lock.json is 1000 entries. Part of #27.
CPython prints # code object from with a repr when it loads a cached pyc and without one when it compiles the source instead, and the cell only handled the first spelling, so it raised IndexError on a checkout with no cached bytecode next to the standard library. Take the marker off the front and strip the quotes if there are any, work the package name out from either an __init__.py or a __pycache__ directory, and print the suffix in the listing so the difference is visible rather than assumed. The claim now says every file holds a frozen name, with the pyc part conditional on the install.
5 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.
R04, the fourth of the nine runtime lessons. R03 finished on a fact it did not explain:
import osnever opensos.py, becauseFrozenImporteris asked beforePathFinderand says yes first. This lesson is about why that module was already in the binary.The problem it solves is a real chicken and egg. The import system is written in Python, in
Lib/importlib/_bootstrap.py, so there is no way to import it. CPython gets around that by compiling that file during its own build, marshalling the code object and writing the bytes out as a C array that the C compiler puts in the binary. The evidence is in the bytecode, and the first cell goes and looks: the module body of_frozen_importlibhas zeroIMPORT_NAMEopcodes in it, which is what makes it loadable with nothing running, while_frozen_importlib_external, which loads second and by then has an import system to use, has eight.init_importlibhandssysand_impto it as arguments, which is why_bootstrap.pynever writesimport sysanywhere.Ten code cells. The thirty three frozen names split into the three arrays
frozen.ckeeps them in, which the flag treats differently: three for the import system that no setting can remove, nineteen for what a bare startup needs, eleven hello world modules for the test suite. A frozen code object pulled straight out of the binary with_imp.get_frozen_objectand weighed.osasked five questions about where it came from, answering<frozen os>to one of them and a real path on disk to another. The flag switched off inside the running interpreter with_imp._override_frozen_modules_for_tests, so you can watchosmove fromFrozenImportertoSourceFileLoaderand back without leaving the notebook. Four steps of a module load timed separately. A startup run twice under-v, counting the files the second one reads that the first one did not.Three things worth calling out.
A frozen module knows perfectly well which file it would have opened.
FrozenImporter.find_specworks the path out fromsys._stdlib_dirand parks it on the spec asloader_state, and the loader then copies it onto__file__. So__spec__.originsaysfrozen,__spec__.cachedsaysNone, and__file__says/some/path/os.py, and all three are true at once. That is the whole reason a traceback through frozen code is readable. Whenlinecachesees a filename that starts with<frozen, it ignores it and reads__file__out of the module globals it was handed instead.The cost is not where you would guess. It is tempting to think frozen wins because unmarshalling from memory beats unmarshalling from a file, but both paths end in the same
marshal.loadsover the same bytes and that call is most of the bill. What freezing removes is the finder search and the file read in front of it. The timing cell measures those four steps separately rather than comparing two totals, and the-vcell shows that every file the second startup reads is a.pycthat had already been compiled, so freezing is not saving compilation either.The two builds disagree about the default, and finding that out is what shaped the experiment.
initconfig.csetsuse_frozen_modulesto zero underPy_DEBUG, so a debug build ships with frozen modules off unless you ask for them. The first version of the Tier 1 program relied on the default and died on the debug build withNo such frozen object named 'os'. The rewritten one never relies on it: it asks for on and off explicitly on every child process and reports what the default happens to be as one more measured fact. Freezing gives back 14.2 percent of a release startup and 9.9 percent of a debug one.Both recordings alternate the two cases round by round rather than running one case forty times and then the other, which is the same lesson C07 and R03 learned the hard way. An early version of the same comparison read 7936 against 14898 microseconds cold and 5910 against 6926 warm, which is a factor of two of pure page cache.
One cell had to be rewritten after the numbers refused to reproduce. It originally claimed loading frozen cost about half of reading the
.pyc, which a standalone probe agreed with at 274.9 against 578.9 microseconds. Under a real IPython kernel the same measurement came back 438.0 against 432.8, a ratio of 1.0. The gap was noise around the unmarshal, which is identical on both paths, so the cell and the claim were both rewritten around what the numbers actually support.Four new glossary terms: frozen module, import bootstrap, loader state and module alias. Six diagrams and twenty one citations. Both READMEs updated. GLOSSARY.md is 226 terms and CLAIMS.md is 589 claims across 75 lessons.
Nine of the ten cells run end to end in a browser. The last one needs a second process, so it prints a line saying so instead.
Part of #27.