curry.backends.cxx.tiered module

Tiered execution of the C++ backend.

Under the interpreter flag interpret set to ‘tiered’, a module without a current compiled object is interpreted from its ICurry at once (see materialize.py and cyrt/icurry.hpp), a child process compiles it in the background, and the runtime replaces the step pointer of every function of the module by the compiled one when the object is ready. A step in flight finishes on the bytecode; the next step of that function runs compiled. The values and the rewrite steps are the same in both tiers. A module with a current object loads compiled, as under ‘new’: the Prelude, which make stage prebuilds, is never interpreted unless its object is missing.

The child. The job of a module runs sprite-make –so on the source file of the module, with the search path of the import and the flag interpret:off, so the plan of the toolchain (Json2Cpp, Cpp2So) writes the object beside the JSON, with its ABI stamp. One worker thread of the runtime (cyrt/tiered.cpp) runs the jobs in the order of submission, one at a time. A module is submitted when its load ends, after the loads of its imports, so the object of an import exists when a module that imports it compiles. The thread needs no GIL: it spawns processes and waits. So the swap reaches a running evaluation: the scheduler applies a finished job at its periodic safepoint (RuntimeState::check_interrupts), and Interpreter.eval applies the finished jobs before an evaluation (poll).

The flags. The child compiles under the inline budget of the interpreter, so the code it swaps in is the code the interpreter ran. The budget shapes the generated code, and the ABI stamp of an object does not record it, so a compile beside the source under a budget other than the one the environment names would leave an object that every later process of that tree takes for current (issue #116). Such a process compiles a copy of the source in a directory of its own (_foreign_flags, _private_copy): the generated file and the object land there, the product cache leaves them out (_productcache.excluded), and they go with the process. An import compiled that way is found by the compile of its importer through the search path of the child, before the original.

The shim. A compiled object names its info tables and data types by ELF symbol, its own included, and the dynamic linker binds a reference to the first definition in the global scope. When the load of a module ends, this module registers a shim for it if the interpreter made any of its tables: a library of absolute symbols, one per table and type made at run time, with the address of the table (g++ -shared -nostdlib over an empty file with –defsym; see Shim). A job carries the shims not yet loaded, and the swap loads them before the object; the loader loads them before any object it loads (ensure_shims). So an object binds every reference to the tables the nodes, the bytecode of other modules, and the Python objects already hold, and one table serves a symbol for the life of the process. The runtime compares tables by address (the equality, the bindings of free variables, curry.inspect.isa), so a second table per symbol is not an option. The load itself is the swap: the dynamic initializer of a static table of the object constructs it at the address its symbol resolves to, which is the table the interpreter uses, so the table carries the compiled step when dlopen returns (Module::adopt counts the functions and takes the rest from the object). A shim is made for the process: the addresses belong to it. The runtime keeps the tables of an interpreted module for the life of the process and gives them back to a module of the same name made after a reset (Module::create_infotable; the reset hands them over at once through Module::retire, whether or not a Python object of the module is alive, issue #114), so a shim stays valid across a reload. A module whose tables changed shape after its shim was made stays interpreted, and so does every module that imports it, because their objects would bind to the old tables; one warning says so. The object of an earlier incarnation of a module stays mapped under its path; the runtime loads the new file through a link of its own name (see the load in cyrt/tiered.hpp).

The imports. A compiled object names the objects of its imports as needed libraries, by their SONAMEs (toolchain.soname), which the dynamic linker satisfies with the objects of those names the process has mapped. So the toolchain loads an object only when every import of the module was loaded from its object (Cpp2So.is_stale imports them first; the loader imports the modules an object needs before it opens it); a module whose import is interpreted is interpreted too, and its compile waits in the queue behind the compile of the import. The swap maps the objects of the imports before the importer’s: a job carries the objects of the imports the interpreter runs (direct and indirect, in dependency order, each with the ICurry digest of its import; see _record and _import_objects), and the swap maps each one whose module has no object yet, under the same check of its ICurry, so an importer swaps whenever the objects are ready: when the job of an import runs later, or when an import has no job (a module without a function to swap, one of type synonyms alone, say, whose object the child of the importer compiles). An import mapped that way is swapped, with no step to count, whatever comes of the load of the importer after it (poll refreshes its record either way); its own job, if any, then adopts the mapped object.

What is not compiled. An expression module (curry.compile in mode ‘expr’), an interactive module, and a module compiled from a string (str2module): the source lives in a temporary directory the interpreter removes, and an expression is evaluated once. A module whose functions are all built in has nothing to swap.

The check of the swap. The child compiles the source as it is when the child runs. An edit of the source between the import and the compile would swap in the code of the edited file, and the module would change meaning without a load (issue #110). So a job carries the digest of the ICurry file (.icy) of the module at the import (cyrt.tiered_file_digest), and the swap refuses the object when the file differs (the result TIERED_EDITED): the module stays interpreted, and poll logs it. A new process reads the edited source. The .icy and not the JSON the import read: a touch or an edit of a comment makes the child run the front end again, which writes the same .icy for the same program, while its JSON differs from the one of the import in its spacing (sprite-make writes the plain form, the import the compact one), so a digest of the JSON refused the swap for an edit that changed nothing.

The load. curry.load of the object of a module that runs interpreted here is a swap as well: the object binds to the live tables through the shim, and an object dropped after such a load leaves the tables pointing into unmapped memory (issue #109). The loader (loader.py) asks the registry by the module name before it opens an object, waits for a pending compile of the module (wait_for) and loads the object the compile wrote, and refuses the object of a module that stays interpreted (has_shim).

Failure. When the compile, the shim, or the load fails, the module stays interpreted, and the failure is logged once per module (poll). The results applied at a safepoint are logged by the next poll: before and after an evaluation, by wait, and at the exit of the process. Interpreter.stats reports the functions swapped and the compiles that failed.

Cancel. A reset, a reload, and the exit of the process drop the queue and kill the running compile, so no compiler outlives the interpreter and a short program never waits for one. The object of a program that ends before its compile is not written; sprite-make –so writes it.

curry.backends.cxx.tiered.cancel()[source]

Drops the queued compiles and kills the running one.

curry.backends.cxx.tiered.counts()[source]

The two counts Interpreter.stats reports: the functions swapped to compiled code and the background compiles that failed.

curry.backends.cxx.tiered.enabled(interp)[source]

Tells whether interp runs tiered execution.

curry.backends.cxx.tiered.ensure_shims(interp)[source]

Loads every registered shim. The loader calls this before it loads a compiled object, so that the object binds its references to the tables in use.

curry.backends.cxx.tiered.has_shim(name)[source]

Tells whether a shim names the tables of module name: the module runs, or ran, interpreted in this process, and an object loaded under the name binds to those tables (see the module docstring).

curry.backends.cxx.tiered.module_loaded(interp, moduleobj, currypath)[source]

Called when the import of a module ends (IBackend.module_loaded). Registers the shim of the module, records its object for the swap of its importers (_record) and queues its background compile. Returns True when a job was queued. currypath is the search path of the imports of the module (IBackend.module_loaded), the CURRYPATH of the child.

curry.backends.cxx.tiered.pending(name)[source]

Tells whether a background compile of module name is queued, running, or finished and not yet applied.

curry.backends.cxx.tiered.poll(interp=None)[source]

Applies the compiled objects that finished in the background, logs the results, and returns them (see cyrtbindings.tiered_results). Call it from the thread that evaluates.

curry.backends.cxx.tiered.status()[source]

The counts of tiered execution in this process: queued, running, swapped_functions, swapped_modules, failed_modules, applied_in_evaluation, and shims.

curry.backends.cxx.tiered.submit(interp, moduleobj, currypath)[source]

Queues the background compile of a module the interpreter runs. Returns True when a job was queued. currypath is the search path of the import. The shim of the module must be registered (module_loaded).

curry.backends.cxx.tiered.wait(timeout=None, interp=None)[source]

Waits until every queued compile ended and its result was applied, or until timeout seconds passed. Returns True when the worker is idle.

curry.backends.cxx.tiered.wait_for(name, timeout=None, interp=None)[source]

Waits until the background compile of module name ended and its result was applied (the compiles queued before it end first), or until timeout seconds passed. Returns True when no compile of the module is pending.