PackageCompiler.jl を使った初回実行高速化

Juliaは初回ロード時にJIT(Just In Time)コンパイルを行っているため、巨大なパッケージをロードする場合、実行に時間がかかってしまいます。2回目以降の実行ではコンパイル済みなのでC並の速度が出ますが、ファイルを実行してデバッグするようなスタイルを取ると実行の度にJuliaセッションが切り替わってしまうため、毎回毎回コンパイルに時間をかけてしまうことになります。

sysimageと呼ばれるセッション保存機能を使うと、JITコンパイル済みの状態を保存することが可能となります。このsysimageを利用することにより、sysimageに含まれるパッケージや関数についてコンパイルの必要がなくなるため、パッケージをリロードして再コンパイルするよりも高速に動作させることが可能となります。

Juliaには起動時にデフォルトで使用されるsysimageが付属しており、Baseという標準ライブラリ等最低限の要素はコンパイル済みで用意されています。(参考:Building the Julia system image

sysimageをカスタムで作成したい!そんな願いを叶えてくれるのが、今回ご紹介するPackageCompiler.jl です。 github.com

sysimageの作成コマンド

sysimageの作成には、PackageCompiler.create_sysimage()を使います。

function create_sysimage(packages::Union{Symbol, Vector{Symbol}}=Symbol[];
                         sysimage_path::Union{String,Nothing}=nothing,
                         project::String=dirname(active_project()),
                         precompile_execution_file::Union{String, Vector{String}}=String[],
                         precompile_statements_file::Union{String, Vector{String}}=String[],
                         incremental::Bool=true,
                         filter_stdlibs=false,
                         replace_default::Bool=false,
                         cpu_target::String=NATIVE_CPU_TARGET,
                         script::Union{Nothing, String}=nothing,
                         base_sysimage::Union{Nothing, String}=nothing,
                         isapp::Bool=false)

覚えておきたい引数リスト

引数
packages パッケージ名のSymbolをリストで指定
sysimage_path 出力するsysimageのパス
precompile_execution_file プリコンパイル対象の関数実行を記載したファイルを指定(後述)
precompile_statements_file コンパイル命令が記載されたファイルを指定(後述)
incremental 追記するか、新規作成するか。デフォルトのsysimageを上書いてしまうと、REPLやPkgのプリコンパイルがされないため、特別な場合を除いてデフォルトのtrueでよい
replace_default デフォルトのsysimageを置き換えるか否か。デフォルトのsysimageに依存しているパッケージが存在するため、基本的には非推奨(後述)

デフォルトのsysimage置き換え

replace_default=true引数により、デフォルトのsysimageを置き換えることも可能ですが、Julia-VSCode等のパッケージがデフォルトのsysimageに依存しているため、非推奨とされています。

なお、初回のデフォルトsysimage置き換え時にバックアップが作成されるため、上書いてしまった場合でもrestore_default_sysimage()コマンドで元のsysimageに戻すことができます。

julia> create_sysimage([:Debugger, :OhMyREPL]; replace_default=true)

※デフォルトのsysimageパスは以下で確認できます。

julia> unsafe_string(Base.JLOptions().image_file)
"/usr/local/src/julia-1.5.2/lib/julia/sys.so"

パッケージのロードを高速化

Exampleパッケージを例にとってコンパイルの行い方を見ていきます。 単純にパッケージをコンパイルしたい場合は、パッケージ名とsysimageの出力先(sysimage_path)だけ指定してあげれば良いです。

julia> using PackageCompiler

# Example パッケージについてExampleSysimage.soという名前のsysimageを作成
julia> PackageCompiler.create_sysimage([:Example], sysimage_path="ExampleSysimage.so")
[ Info: PackageCompiler: creating system image object file, this might take a while...

作成したsysimageを使用してJuliaを起動する際は -J, --sysimage で指定します。

$ julia -JExampleSysimage.so

# ロードが早くなる!
julia> using Example

関数のロードを高速化

PackageCompilerでは、パッケージのロードに加え、内部の関数についても事前にコンパイルすることが可能です。関数のコンパイルには、以下の2つの方法を取ることができます。

  1. 関数実行のスクリプトをprecompile_execution_fileとして指定する
  2. コンパイル命令をprecompile_statements_fileとして指定する

1. 関数実行のスクリプトを使用

例として、precompile_example.jlという以下のスクリプトを作成します。

$ cat precompile_example.jl
using Example
Example.hello("friend")

precompile_execution_fileに作成したスクリプトを指定してsysimageを作成することで、関数を含めてコンパイルすることができます。

julia> using PackageCompiler

# Example パッケージ及びprecompile_example.jlに含まれる関数についてExampleSysimagePrecompile.soという名前のsysimageを作成
julia> PackageCompiler.create_sysimage(:Example; sysimage_path="ExampleSysimagePrecompile.so",
                                         precompile_execution_file="precompile_example.jl")
[ Info: PackageCompiler: creating system image object file, this might take a while...

2. コンパイル命令を使用

trace-compileの出力を利用

Juliaを--trace-compile = file.jl引数を付けて実行すると、Juliaプロセスの実行中にコンパイルされた関数のコンパイル命令をfile.jlに出力することができます。 コンパイル対象のコードが記述しづらい場合(インタラクティブシェルの起動をトリガーに呼ばれる関数等)に、この引数を用いた方法は便利です。
OhMyREPLの例が公式にあるので参考にどうぞ。

なお、--trace-compileの出力は標準出力にも出せます。どのようなコンパイルが行われているかサクッと確認したい場合は標準出力で確認するのが良いでしょう。

$ julia --trace-compile=stderr -e 'import Example' --startup-file=no                                  (base)
precompile(Tuple{typeof(fzf_jll.__init__)})
precompile(Tuple{getfield(Core, Symbol("#Type##kw")), NamedTuple{(:libc, :compiler_abi), Tuple{Nothing, Pkg.BinaryPlatforms.CompilerABI}}, Type{Pkg.BinaryPlatforms.FreeBSD}, Symbol})
precompile(Tuple{Type{Base.Pair{A, B} where B where A}, Pkg.BinaryPlatforms.FreeBSD, Base.Dict{String, Any}})
precompile(Tuple{typeof(Base.setindex!), Base.Dict{Pkg.BinaryPlatforms.Platform, Base.Dict{String, Any}}, Base.Dict{String, Any}, Pkg.BinaryPlatforms.FreeBSD})
precompile(Tuple{typeof(JLLWrappers.get_julia_libpaths)})
precompile(Tuple{typeof(OhMyREPL.__init__)})

テストスイートを利用

以下のようなテストスイートをコンパイル追跡対象とすることで、使用される関数について網羅的なコンパイル文を生成することができます。

import Example
include(joinpath(pkgdir(Example), "test", "runtests.jl"))

テストスイートのコンパイル文をcompile_example.jlに出力

$ julia --trace-compile=compile_example.jl -e 'import Example; include(joinpath(pkgdir(Example), "test", "run
tests.jl"))'

コンパイル文からsysimageを生成

julia> using PackageCompiler

# Example パッケージ及びcompile_example.jlに含まれるコンパイル文についてExampleSysimagePrecompile.soという名前のsysimageを作成
julia> PackageCompiler.create_sysimage(:Example; sysimage_path="ExampleSysimagePrecompile.so",
                                       precompile_statements_file="compile_example.jl")
[ Info: PackageCompiler: creating system image object file, this might take a while...

Example パッケージを用いたベンチマーク

sysimageを作成

# ~/.julia/config/startup.jl を読まないようにする
$ julia -q --startup-file=no

julia> using PackageCompiler

# 仮想環境を作り、Exampleパッケージをインストール
(@v1.5) pkg> activate .
 Activating new environment at `~/test/Project.toml`
 
(test) pkg> add Example
   Updating registry at `~/.julia/registries/General`
  Resolving package versions...
  Installed Example ─ v0.5.3
Updating `~/test/Project.toml`
  [7876af07] + Example v0.5.3
Updating `~/test/Manifest.toml`
  [7876af07] + Example v0.5.3

# "ExampleSysimage.so"というファイル名でsysimageファイルを作成
julia> create_sysimage(:Example; sysimage_path="ExampleSysimage.so")
[ Info: PackageCompiler: creating system image object file, this might take a while...

julia> exit()

sysimageを利用しない場合

# sysimageファイルを指定せずREPL起動
$ julia -q --startup-file=no

# 普通に読み込んでみると1sec以上かかる
julia> @time using Example
  1.203322 seconds (652.89 k allocations: 37.429 MiB, 5.15% gc time)

sysimageを利用した場合

# -Jで作成したsysimageファイルを指定してREPL起動
$ julia -q --startup-file=no -JExampleSysimage.so

# 起動直後からload済みとなっている
julia> Base.loaded_modules
Dict{Base.PkgId,Module} with 34 entries:
...
  Example [7876af07-990d-54b4-ab0e-23690620f79a]          => Example
...

# めちゃ早い
julia> @time using Example
  0.000259 seconds (203 allocations: 12.609 KiB)

その他

毎回決まったsysimageを読み込みたい!

juliaコマンドのエイリアスを作りましょう。

$ alias julia = 'julia -J /path/to/sysimage.so'