System Image Building
Building the Julia system image
Juliaは、Baseモジュールの内容を含む事前解析されたシステムイメージを持っており、これをsys.jiと呼びます。このファイルは、可能な限り多くのプラットフォームで、sys.{so,dll,dylib}という共有ライブラリに事前コンパイルされています。これにより、起動時間が大幅に改善されます。事前コンパイルされたシステムイメージファイルが付属していないシステムでは、JuliaのDATAROOTDIR/julia/baseフォルダーに付属するソースファイルから生成できます。
Juliaはデフォルトで利用可能なシステムスレッドの半分でシステムイメージを生成します。これは、JULIA_IMAGE_THREADS 環境変数によって制御できます。
この操作は複数の理由で便利です。ユーザーは:
- プラットフォームにプリコンパイルされた共有ライブラリシステムイメージが付属していない場合、スタートアップ時間を改善するためにそれを構築します。
Baseを修正し、システムイメージを再構築して、次回Juliaが起動したときに新しいBaseを使用します。userimg.jlファイルを含めて、パッケージをシステムイメージに組み込むことで、スタートアップ環境に埋め込まれたパッケージを持つシステムイメージを作成します。
PackageCompiler.jl package には、このプロセスを自動化する便利なラッパー関数が含まれています。
System image optimized for multiple microarchitectures
システムイメージは、同じ命令セットアーキテクチャ(ISA)の下で複数のCPUマイクロアーキテクチャ用に同時にコンパイルできます。同じ関数の複数のバージョンが、異なるISA拡張や他のマイクロアーキテクチャ機能を活用するために、共有関数に最小ディスパッチポイントを挿入して作成される場合があります。最もパフォーマンスが良いバージョンは、利用可能なCPU機能に基づいてランタイムで自動的に選択されます。
Specifying multiple system image targets
マルチマイクロアーキテクチャシステムイメージは、システムイメージのコンパイル中に複数のターゲットを渡すことで有効にできます。これは、JULIA_CPU_TARGET メイクオプションを使用するか、コンパイルコマンドを手動で実行する際に -C コマンドラインオプションを使用することで行えます。複数のターゲットは、オプション文字列内で ; で区切られます。各ターゲットの構文は、CPU名の後に複数の機能が , で区切られて続きます。LLVMによってサポートされているすべての機能がサポートされており、機能は - プレフィックスで無効にできます。(+ プレフィックスも許可されており、LLVMの構文と一貫性を持たせるために無視されます)。さらに、関数クローンの動作を制御するために、いくつかの特別な機能がサポートされています。
最初のターゲット以外のすべてのターゲットに対して、clone_allまたはbase(<n>)のいずれかを指定することは良いプラクティスです。これにより、すべての関数がクローンされたターゲットと、他のターゲットに基づいているターゲットが明示的になります。これが行われない場合、デフォルトの動作はすべての関数をクローンせず、関数をクローンしない場合は最初のターゲットの関数定義をフォールバックとして使用します。
clone_allデフォルトでは、マイクロアーキテクチャの機能から最も恩恵を受ける可能性が高い関数のみがクローンされます。しかし、ターゲットに対して
clone_allが指定されると、システムイメージ内の すべて の関数がターゲットのためにクローンされます。負の形式-clone_allを使用すると、組み込みのヒューリスティックがすべての関数をクローンするのを防ぐことができます。base(<n>)ここで
<n>は非負の数のプレースホルダーです(例:base(0)、base(1))。 デフォルトでは、部分的にクローンされた(すなわちclone_allではない)ターゲットは、関数がクローンされていない場合、デフォルトターゲット(最初に指定されたもの)から関数を使用します。この動作は、base(<n>)オプションを指定することで変更できます。n番目のターゲット(0から始まる)がデフォルトのターゲット(0番目のもの)ではなく、ベースターゲットとして使用されます。 ベースターゲットは0か、別のclone_allターゲットでなければなりません。 非clone_allターゲットをベースターゲットとして指定するとエラーが発生します。opt_sizeこれは、実行時のパフォーマンスに大きな影響がない場合に、ターゲットの関数がサイズの最適化のために最適化されることを引き起こします。これは
-OsGCC および Clang オプションに対応します。min_sizeこれにより、ターゲットの関数がサイズの最適化のために調整され、実行時のパフォーマンスに大きな影響を与える可能性があります。これは
-OzClang オプションに対応しています。
この執筆時点での例として、公式の x86_64 Julia バイナリを julialang.org からダウンロードする際に使用される以下の文字列があります:
generic;sandybridge,-xsaveopt,clone_all;haswell,-rdrnd,base(1)これにより、3つの異なるターゲットを持つシステムイメージが作成されます。1つは一般的な x86_64 プロセッサ用、1つは sandybridge ISA(xsaveopt を明示的に除外)で、すべての関数を明示的にクローンし、1つは sandybridge sysimg バージョンに基づく haswell ISA をターゲットにし、rdrnd も除外します。Julia 実装が生成された sysimg をロードすると、ホストプロセッサの CPU 機能フラグをチェックし、可能な限り高い ISA レベルを有効にします。基本レベル(generic)は cx16 命令を必要とし、これは一部の仮想化ソフトウェアでは無効になっており、generic ターゲットをロードするには有効にする必要があります。あるいは、より互換性を高めるために generic,-cx16 ターゲットで sysimg を生成することもできますが、これにより一部のコードでパフォーマンスや安定性の問題が発生する可能性があることに注意してください。
Implementation overview
これは、実装に関与するさまざまな部分の簡単な概要です。各コンポーネントの詳細な実装については、コードコメントを参照してください。
システムイメージのコンパイル
src/processor*でパースとクローンの決定が行われます。現在、ループ、SIMD命令、またはその他の数学演算(例:fastmath、fma、muladd)の存在に基づいて関数のクローンをサポートしています。この情報は、実際のクローンを行うsrc/llvm-multiversioning.cppに渡されます。クローンを行い、ディスパッチスロットを挿入する(これがどのように行われるかはMultiVersioning::runOnModuleのコメントを参照)だけでなく、このパスはメタデータも生成し、ランタイムがシステムイメージを正しくロードおよび初期化できるようにします。メタデータの詳細な説明はsrc/processor.hにあります。システムイメージの読み込み
システムイメージの読み込みと初期化は、システムイメージ生成中に保存されたメタデータを解析することによって
src/processor*で行われます。ホスト機能の検出と選択の決定は、ISAに応じてsrc/processor_*.cppで行われます。ターゲット選択は、正確なCPU名の一致、より大きなベクタレジスタサイズ、およびより多くの機能を優先します。このプロセスの概要はsrc/processor.cppにあります。
Trimming
システムイメージは通常非常に大きく、Baseには多くの機能が含まれており、デフォルトではシステムイメージには便利さと後方互換性のためにLinearAlgebraなどのいくつかのパッケージも含まれています。ほとんどのプログラムはこれらのパッケージの関数のほんの一部しか使用しません。したがって、未使用の関数を除外してスペースを節約するバイナリを構築することは理にかなっており、これを「トリミング」と呼びます。
基本的なトリミングのアイデアは理にかなっていますが、Juliaには動的および反射的な機能があり、一般的にどの関数が未使用であるかを知ることが難しい(または不可能)です。極端な例として、次のようなコードを考えてみてください。
getglobal(Base, Symbol(readchomp(stdin)))(1)このコードは、stdin から関数名を読み取り、値 1 に基づいて Base から指定された関数を呼び出します。この場合、どの関数が呼び出されるかを予測することは不可能であるため、信頼できる「未使用」と見なされる関数はありません。いくつかの注目すべき例外(Julia の REPL 自体がその一つです)を除いて、ほとんどの実際のプログラムはこのようなことを行いません。
より極端でないケースは、例えば、コンパイラがどのメソッドが呼び出されるかを予測できない型の不安定性がある場合に発生します。しかし、コードが適切に型付けされていてリフレクションを使用していない場合、必要なメソッドの完全で(望ましくは)比較的小さなセットを特定でき、残りは削除できます。--trim コマンドラインオプションは、この種のコンパイルを要求します。
--trimがシステムイメージをビルドするために使用されるコマンドで指定されると、コンパイラはBase.Experimental.entrypointでマークされたメソッドから呼び出しのトレースを開始します。呼び出しが可能なターゲットを合理的に絞り込むにはあまりにも動的な場合、呼び出しの位置を示すコンパイル時エラーが表示されます。テスト目的で、--trim=unsafeまたは--trim=unsafe-warnを指定することで、これらのエラーをスキップすることが可能です。そうすると、システムイメージがビルドされますが、必要なコードが存在しない場合、実行時にクラッシュする可能性があります。
通常、--trimと一緒に--strip-irを指定することは理にかなっています。なぜなら、トリミングされたバイナリは完全にコンパイルされているため、Julia IRを必要としないからです。いずれは--trimが--strip-irを暗黙的に含むようにするかもしれませんが、現時点ではそれらを独立させています。
最小のバイナリを得るためには、--strip-metadataを指定し、Unixのstripユーティリティを実行することも役立ちます。ただし、これらの手順はそれぞれJulia特有のデバッグ情報とネイティブ(DWARF形式)のデバッグ情報を削除するため、デバッグが難しくなります。
Common problems
- Baseのグローバル変数
stdin、stdout、およびstderrは非定数であり、その型は不明です。すべての出力は、既知の型を持つ特定のIOオブジェクトを使用する必要があります。最も簡単な置き換えは、print(Core.stdout, x)を使用することです。これにより、print(x)やprint(stdout, x)の代わりになります。 - ツールを使用して、JET.jl、Cthulhu.jl、および/またはSnoopCompileを使用して型推論の失敗を特定し、Performance Tipsに従ってそれらを修正してください。
Compatibility concerns
私たちは、信頼性のあるトリミングが可能なプログラムのセットを大幅に増加させるBaseの多くの小さな変更を特定しました。残念ながら、これらの変更のいくつかは破壊的と見なされるため、トリミングが要求されたときのみ適用されます(これは外部ビルドスクリプトによって行われ、現在はテストスイート内のcontrib/juliac/juliac-buildscript.jlに維持されています)。したがって、多くの場合、トリミングには新しいBaseのバリアントやいくつかの標準ライブラリにオプトインする必要があります。
トリミングを使用したい場合は、トリミングビルドを実行し、結果として得られたプログラムを完全にテストする継続的インテグレーションテストを設定することが重要です。幸いなことに、プログラムが --trim で正常にコンパイルされる場合、それは以前と同じように動作する可能性が非常に高いです。しかし、CIは、開発を進める中でプログラムがトリミングでビルドし続けることを保証するために必要です。
パッケージの著者は、自分のパッケージが「トリミングセーフ」であることをテストしたいと考えるかもしれませんが、一般的にはこれは不可能です。トリミングは、main() や Julia の外部から呼び出されることを意図したライブラリのエントリポイントなど、具体的なエントリポイントがある場合にのみ機能することが期待されます。汎用パッケージの場合、@inferred や JET.@report_call のような型安定性の既存のテストが、トリム互換性をチェックするためにできる限り近いものです。
トリミングは、Juliaのマイナーバージョン間で新たな互換性の問題を引き起こすこともあります。この時点では、あるバージョンのJuliaでトリミングできるプログラムが、将来のすべてのバージョンのJuliaでもトリミングできることを保証することはできません。ただし、そのような破損は稀であると予想されています。また、時間の経過とともにトリミングできるプログラムのセットを増やすことを試みる予定です。