PackageCompiler.jl

repository·master·Indexed 23 days ago

https://github.com/julialang/packagecompiler.jl

A Julia tool for optimizing startup performance and distributing applications. It provides capabilities to create custom sysimages to reduce latency, bundle code into standalone executables for machines without Julia installed, and compile Julia code into relocatable C libraries. The package includes high-level interfaces like create_sysimage and create_app, and supports the Julia artifact system to ensure app relocatability.

Tokens
9.4K
Snippets
16
Records
43
Agent score
81%

What's inside PackageCompiler.jl

  1. Overview of PackageCompiler capabilities

    master

    PackageCompiler is a Julia package designed to optimize and distribute Julia code through three primary methods:

    1. Custom sysimages: Reduces latency for local development by pre-compiling packages that have high startup times into a system image.
    2. Standalone Apps: Bundles your code into an executable and a set of files that can be distributed and run on machines that do not have Julia installed.
    3. Relocatable C libraries: Compiles Julia code into a bundle that can be used as a relocatable C library.
  2. Core functionalities of PackageCompiler.jl

    master

    PackageCompiler.jl provides high-level interfaces for three primary tasks related to Julia deployment and performance:

    1. Reducing load times and latency: Use create_sysimage to build a local system image. This pre-compiles packages to reduce the time it takes to load them and minimizes the latency experienced when calling a function for the currently uncompiled code for the first time.
    2. Creating standalone executables: Use create_app to build an executable based on a custom sysimage. This allows you to run your code without explicitly starting a Julia session.
    3. Bundling for distribution: Use create_app to bundle the executable together with the necessary Julia libraries and files. This creates a package that can be sent to and run on different systems where Julia is not installed.
  3. What is a sysimage and when to use one

    master

    A sysimage is a serialized Julia session containing loaded packages, global variables, and compiled code. Starting Julia with a sysimage is faster than reloading packages and recompiling code from scratch.

    When to use custom sysimages

    • When dependencies have significant load times.
    • When the compilation time for the first call to a function is uncomfortably long.
    • For "workflow packages" like Revise.jl or OhMyREPL.jl.

    Drawbacks and Risks

    • Version Locking: Packages (and their dependencies) compiled into a sysimage are "locked" to the versions present at the time of creation. The sysimage version will take precedence over any version installed in your current project, which can lead to version conflicts and bugs.
    • Maintenance: Only use sysimages for packages that are not frequently updated.
  4. What is a sysimage and why use custom sysimages?

    master

    A sysimage is a shared library that stores the serialized state of a running Julia session. When Julia starts, loading a sysimage allows the system to use cached compiled code immediately, significantly reducing the time between startup and program execution (latency).

    When to use custom sysimages

    You should create a custom sysimage if your goal is to minimize the latency of a program by pre-loading specific packages and their compiled states.

    Important Trade-offs:

    • Freezing Versions: Any package included in a sysimage is "frozen" at the specific version it was when the sysimage was created. All of its dependencies are also frozen. These packages will no longer be updated via the standard Julia package manager.
    • Alternatives: If you only need to reduce latency during development, tools like Revise.jl might be preferable to the complexity of managing sysimages.
  5. Understanding Julia's compilation and precompilation

    master

    Julia uses a Just-Ahead-of-Time (JAOT) compilation model where functions are compiled just before execution.

    Precompilation vs. Sysimages

    • Precompilation: When you run using PackageName, Julia performs precompilation. This caches parsed and type-inferred information, which reduces loading time for subsequent sessions, but it does not eliminate the compilation overhead that occurs during the first actual function call.
    • Sysimages: A sysimage goes a step further by capturing the state of the session (including compiled code) into a shared library. This aims to eliminate the runtime compilation latency seen during the first call to a function in a standard Julia session.
  6. Understand the three PackageCompiler workflows

    master

    PackageCompiler provides three distinct ways to perform ahead-of-time compilation to reduce latency or enable distribution:

    1. Sysimages: Saves loaded packages and compiled functions into a file (a sysimage) that you pass to julia via the --sysimage=<PATH/TO/SYSIMAGE> flag at startup. This is primarily used to reduce startup latency on your local machine. Note: Sysimages are generally not relocatable; they only work on the machine where they were created.

    2. Relocatable Apps: Compiles an entire project into a bundle of files, including an executable. This bundle includes Julia itself and all dependencies (including cross-platform binary libraries). This can be sent to other machines that may not have Julia installed.

    3. C Libraries: Creates a redistributable directory structure containing Julia and its dependencies. Your package must define C-callable functions to be included. These libraries can be moved to other machines of the same architecture.

    How compilation is determined: Because it is impossible to compile every possible type combination, PackageCompiler uses "tracing" an exemplar session. It records which methods were used during a session and compiles those. Any methods missed during tracing will be compiled on-demand (by default) when they are first called.

  7. How library preferences work

    master

    PackageCompiler handles both compile-time and runtime preferences for packages included in your library.

    • Compile-time preferences: These are baked into the sysimage during the create_library process.
    • Runtime preferences: To support runtime changes, all preferences visible during compilation are stored in the library bundle at <dest_dir>/share/julia/LocalPreferences.toml.

    Note: Modifying LocalPreferences.toml will not affect preferences that were already loaded at compile time, but it will change the values of preferences loaded at runtime by the library.

  8. Understanding relocatability issues in Julia packages

    master

    A common obstacle to creating relocatable Julia applications is when packages encode absolute file paths directly into their source code during the build process.

    For example, a package might use a build.jl script to find a library path and write it to a file like deps.jl:

    lib_path = find_library("libfoo")
    write("deps.jl", "const LIBFOO_PATH = $(repr(lib_path))")

    If this package is included in a sysimage and then moved to a different machine, the absolute path stored in LIBFOO_PATH will likely be invalid, causing the package to fail during initialization (e.g., when calling Libdl.dlopen(LIBFOO_PATH)).

    To ensure packages are relocatable, avoid hardcoding absolute paths in source files. Instead, use the Julia artifact system.

  9. Install PackageCompiler

    master

    Install PackageCompiler using the standard Julia package manager.

    Important Requirements:

    • Julia Installation: It is strongly recommended to use official binaries from julialang.org/downloads/. Distribution-provided Julia installations may not work correctly.
    • C Compiler: A C compiler must be available on your system.

    macOS and Linux

    A modern gcc or clang is sufficient. On macOS, you can use Xcode command line tools or homebrew. On Linux, use your system package manager to install a compiler.

    Windows

    A suitable compiler will be automatically installed the first time it is needed.

    using Pkg
    Pkg.add("PackageCompiler")
  10. Embed Julia in a C application

    master

    To drive a Julia application from C, your embedding script must:

    1. Initialize the Julia runtime using julia_init.
    2. Manually set Base.ARGS and Base.PROGRAM_FILE to match the command-line arguments passed to the executable.
    3. Call the @ccallable entry point.
    4. Perform cleanup using jl_atexit_hook.
    #include "uv.h"
    #include "julia.h"
    
    JULIA_DEFINE_FAST_TLS()
    int julia_main();
    
    int main(int argc, char *argv[])
    {
        uv_setup_args(argc, argv);
        jl_options.image_file = JULIAC_PROGRAM_LIBNAME;
        julia_init(JL_IMAGE_JULIA_HOME);
        jl_set_ARGS(argc, argv);
    
        // Set PROGRAM_FILE
        jl_sym_t *var = jl_symbol("PROGRAM_FILE");
        jl_value_t *val = jl_cstr_to_string(argv[0]);
    #if JULIA_VERSION_MAJOR == 1 && JULIA_VERSION_MINOR >= 10
        jl_binding_t *bp = jl_get_binding_wr(jl_base_module, var);
        jl_checked_assignment(bp, jl_base_module, var, val);
    #elif JULIA_VERSION_MAJOR == 1 && JULIA_VERSION_MINOR >= 9
        jl_binding_t *bp = jl_get_binding_wr(jl_base_module, var, 1);
        jl_checked_assignment(bp, val);
    #else
        jl_set_global(jl_base_module, var, val);
    #endif
    
        // Set Base.ARGS
        jl_array_t *ARGS = (jl_array_t*)jl_get_global(jl_base_module, jl_symbol("ARGS"));
        jl_array_grow_end(ARGS, argc - 1);
        for (int i = 1; i < argc; i++) {
            jl_value_t *s = (jl_value_t*)jl_cstr_to_string(argv[i]);
            jl_arrayset(ARGS, s, i - 1);
        }
    
        int ret = julia_main();
        jl_atexit_hook(ret);
        return ret;
    }
  11. Create a C-callable entry point in Julia

    master

    To allow a C program to call into your Julia code, you must define a function annotated with Base.@ccallable. This ensures the function name remains unmangled in the resulting sysimage. It is a best practice to wrap your main logic in a try-catch block within this entry point to ensure errors are handled gracefully and provide useful stack traces, rather than crashing the C host.

    module MyApp
    
    using CSV
    
    Base.@ccallable function julia_main()::Cint
        try
            real_main()
        catch
            Base.invokelatest(Base.display_error, Base.catch_stack())
            return 1
        end
        return 0
    end
    
    function real_main()
        for file in ARGS
            if !isfile(file)
                error("could not find file $file")
            end
            df = CSV.read(file)
            println(file, ": ", size(df, 1), "x", size(df, 2))
        end
    end
    
    if abspath(PROGRAM_FILE) == @__FILE__
        real_main()
    end
    
    end # module