JIT Design and Implementation
この文書は、コード生成が完了し、最適化されていないLLVM IRが生成された後のJuliaのJITの設計と実装について説明しています。JITは、このIRを最適化して機械コードにコンパイルし、現在のプロセスにリンクしてコードを実行可能にする責任を負っています。
Introduction
JITは、コンパイルリソースの管理、以前にコンパイルされたコードの検索、新しいコードのコンパイルを担当しています。これは主にLLVMのOn-Request-Compilation(ORCv2)技術に基づいており、同時コンパイル、遅延コンパイル、別プロセスでのコードコンパイルの能力など、いくつかの便利な機能をサポートしています。LLVMはLLJITの形で基本的なJITコンパイラを提供していますが、Juliaは多くのORCv2 APIを直接使用して独自のカスタムJITコンパイラを作成しています。
Overview
Codegenは、型推論によって生成された元のJulia SSA IRから1つ以上のJulia関数のIRを含むLLVMモジュールを生成します(上記のコンパイラ図でtranslateとしてラベル付けされています)。また、コードインスタンスとLLVM関数名のマッピングも生成します。しかし、JuliaベースのコンパイラがJulia IRに対していくつかの最適化を適用したにもかかわらず、codegenによって生成されたLLVM IRにはまだ多くの最適化の機会が含まれています。したがって、JITが最初に行うステップは、LLVMモジュールに対してターゲット非依存の最適化パイプライン[tdp]を実行することです。次に、JITはターゲット依存の最適化パイプラインを実行し、ターゲット固有の最適化とコード生成を含み、オブジェクトファイルを出力します。最後に、JITは生成されたオブジェクトファイルを現在のプロセスにリンクし、コードを実行可能にします。これらすべては、src/jitlayers.cpp
のコードによって制御されています。
現在、最適化-コンパイル-リンクパイプラインには、一度に1つのスレッドのみが入ることが許可されています。これは、私たちのリンカーの1つ(RuntimeDyld)によって課せられた制限によるものです。しかし、JITは同時最適化とコンパイルをサポートするように設計されており、RuntimeDyldがすべてのプラットフォームで完全に置き換えられたときに、リンカーの制限は解除されると予想されています。
Optimization Pipeline
最適化パイプラインはLLVMの新しいパスマネージャに基づいていますが、パイプラインはJuliaのニーズに合わせてカスタマイズされています。パイプラインはsrc/pipeline.cpp
で定義されており、以下に詳述するいくつかのステージを通じて進行します。
初期の簡素化
- これらのパスは、主にIRを簡素化し、パターンを正規化するために使用され、後のパスがそれらのパターンをより簡単に特定できるようにします。さらに、分岐予測ヒントや注釈などのさまざまな内在的呼び出しは、他のメタデータや他のIR機能に低下されます。
SimplifyCFG
(制御フローグラフの簡素化)、DCE
(デッドコードの排除)、およびSROA
(アグリゲートのスカラー置換)は、ここでの重要なプレーヤーのいくつかです。
- これらのパスは、主にIRを簡素化し、パターンを正規化するために使用され、後のパスがそれらのパターンをより簡単に特定できるようにします。さらに、分岐予測ヒントや注釈などのさまざまな内在的呼び出しは、他のメタデータや他のIR機能に低下されます。
早期最適化
- これらのパスは通常安価であり、主にIR内の命令数を減らし、他の命令に知識を伝播させることに焦点を当てています。例えば、
EarlyCSE
は共通部分式の排除を行うために使用され、InstCombine
およびInstSimplify
は、操作をより安価にするためのいくつかの小さなピーphole最適化を実行します。
- これらのパスは通常安価であり、主にIR内の命令数を減らし、他の命令に知識を伝播させることに焦点を当てています。例えば、
ループ最適化
- これらのパスはループを正準化し、簡素化します。ループはしばしばホットコードであり、ループ最適化はパフォーマンスにとって非常に重要です。ここでの主要なプレーヤーには
LoopRotate
、LICM
、およびLoopFullUnroll
が含まれます。また、IRCE
パスの結果として、特定の境界が決して超えられないことを証明できるため、いくつかの境界チェックの排除もここで発生します。
- これらのパスはループを正準化し、簡素化します。ループはしばしばホットコードであり、ループ最適化はパフォーマンスにとって非常に重要です。ここでの主要なプレーヤーには
スカラー最適化
ベクトル化
- Automatic vectorization は、CPU集中的なコードに対して非常に強力な変換です。簡単に言うと、ベクトル化は single instruction on multiple data (SIMD) の実行を可能にし、例えば8つの加算操作を同時に行うことができます。しかし、コードがベクトル化可能であり、かつベクトル化する価値があることを証明するのは難しく、これは主に事前の最適化パスに依存してIRをベクトル化する価値のある状態に整えることに依存しています。
内因的低下
- Juliaは、オブジェクトの割り当て、ガーベジコレクション、例外処理などの理由から、いくつかのカスタムインタリンズを挿入します。これらのインタリンズは、最適化の機会をより明確にするために元々配置されましたが、現在はLLVM IRに低下され、IRが機械コードとして出力されることを可能にしています。
クリーンアップ
- これらのパスは最後のチャンスの最適化であり、融合乗算加算伝播や除算余り簡略化などの小さな最適化を実行します。さらに、半精度浮動小数点数をサポートしないターゲットでは、半精度命令がここで単精度命令に変換され、サニタイザサポートを提供するためのパスが追加されます。
Target-Dependent Optimization and Code Generation
LLVMは、特定のプラットフォームのTargetMachine内で、ターゲット依存の最適化と機械コード生成を同じパイプラインで提供します。これらのパスには、命令選択、命令スケジューリング、レジスタ割り当て、および機械コードの発行が含まれます。LLVMのドキュメントはこのプロセスの良い概要を提供しており、LLVMのソースコードはパイプラインとパスの詳細を確認するのに最適な場所です。
Linking
現在、Juliaは古いRuntimeDyldリンカーと新しいJITLinkリンカーの間を移行しています。JITLinkには、RuntimeDyldにはないいくつかの機能が含まれており、同時リンクや再入可能リンクなどがありますが、現在はプロファイリング統合の良好なサポートが欠けており、RuntimeDyldがサポートするすべてのプラットフォームをまだサポートしていません。時間が経つにつれて、JITLinkはRuntimeDyldを完全に置き換えることが期待されています。JITLinkに関する詳細はLLVMのドキュメントに記載されています。
Execution
コードが現在のプロセスにリンクされると、それは実行可能になります。この事実は、invoke
、specsigflags
、および specptr
フィールドを適切に更新することによって生成コードインスタンスに知らされます。コードインスタンスは、これらのフィールドのすべての組み合わせが任意の時点で有効である限り、invoke
、specsigflags
、および specptr
フィールドのアップグレードをサポートします。これにより、JITは既存のコードインスタンスを無効にすることなくこれらのフィールドを更新でき、将来の並行JITをサポートします。具体的には、以下の状態が有効である可能性があります:
invoke
は NULL であり、specsigflags
は 0b00 であり、specptr
は NULL です。- これはコードインスタンスの初期状態であり、コードインスタンスがまだコンパイルされていないことを示しています。
invoke
は非NULLで、specsigflags
は0b00、specptr
はNULLです。- これは、codeinstが特別化なしでコンパイルされておらず、codeinstを直接呼び出すべきであることを示しています。この場合、
invoke
はspecsigflags
またはspecptr
フィールドを読み取らないため、これらはinvoke
ポインタを無効にすることなく変更できます。
- これは、codeinstが特別化なしでコンパイルされておらず、codeinstを直接呼び出すべきであることを示しています。この場合、
invoke
は非null、specsigflags
は0b10、specptr
は非nullです- これは、codeinstがコンパイルされたことを示していますが、codegenによって特別な関数シグネチャは不要と見なされました。
invoke
は非null、specsigflags
は0b11、specptr
は非nullです- これは、codeinstがコンパイルされ、codegenによって特化した関数シグネチャが必要であると見なされたことを示しています。
specptr
フィールドには、特化した関数シグネチャへのポインタが含まれています。invoke
ポインタは、specsigflags
およびspecptr
フィールドの両方を読み取ることが許可されています。
- これは、codeinstがコンパイルされ、codegenによって特化した関数シグネチャが必要であると見なされたことを示しています。
さらに、更新プロセス中に発生するさまざまな遷移状態があります。これらの潜在的な状況に対処するために、これらの codeinst フィールドを扱う際には、以下の書き込みおよび読み取りパターンを使用する必要があります。
invoke
、specsigflags
、およびspecptr
を記述する際:- NULLだったと仮定してspecptrの原子比較交換操作を実行します。この比較交換操作は、書き込みの残りのメモリ操作に対する順序保証を提供するために、少なくとも取得-解放順序を持つ必要があります。
specptr
が非NULLの場合、書き込み操作を中止し、specsigflags
のビット0b10が書き込まれるのを待ちます。specsigflags
の新しい低ビットを最終値に書き込みます。これは緩やかな書き込みである可能性があります。- 新しい
invoke
ポインタを最終値に書き込みます。これは、invoke
の読み取りと同期するために、少なくともリリースメモリ順序を持っている必要があります。 specsigflags
の2番目のビットを1に設定します。これは、specsigflags
の読み取りと同期するために、少なくともリリースメモリ順序である必要があります。このステップは書き込み操作を完了し、すべての他のスレッドにすべてのフィールドが設定されたことを通知します。
invoke
、specsigflags
、およびspecptr
をすべて読むとき:invoke
フィールドを少なくとも取得メモリ順序で読み取ります。このロードはinitial_invoke
と呼ばれます。initial_invoke
がNULLの場合、codeinstはまだ実行可能ではありません。invoke
はNULLであり、specsigflags
は0b00として扱うことができ、specptr
はNULLとして扱うことができます。specptr
フィールドを少なくとも取得メモリ順序で読み取ります。specptr
がNULLの場合、initial_invoke
ポインタは正しい実行を保証するためにspecptr
に依存してはなりません。したがって、invoke
は非NULLであり、specsigflags
は0b00として扱うことができ、specptr
はNULLとして扱うことができます。specptr
が非NULLである場合、initial_invoke
はspecptr
を使用する最終的なinvoke
フィールドではない可能性があります。これは、specptr
が書き込まれたが、invoke
がまだ書き込まれていない場合に発生する可能性があります。したがって、少なくとも取得メモリ順序で1に設定されるまで、specsigflags
の2番目のビットでスピンします。invoke
フィールドを少なくとも取得メモリ順序で再読み込みします。このロードはfinal_invoke
と呼ばれます。specsigflags
フィールドを任意のメモリ順序で読み取ります。invoke
はfinal_invoke
であり、specsigflags
はステップ 7 で読み取った値で、specptr
はステップ 3 で読み取った値です。
specptr
を異なるが同等の関数ポインタに更新する場合:- 新しい関数ポインタを
specptr
にリリースストアを実行します。ここでの競合は無害でなければならず、古い関数ポインタは依然として有効である必要があり、新しいポインタも有効である必要があります。specptr
にポインタが書き込まれた後は、それが後で上書きされるかどうかにかかわらず、常に呼び出し可能でなければなりません。
- 新しい関数ポインタを
これらの書き込み、読み取り、および更新のステップは複雑ですが、JITが既存のcodeinstを無効にすることなくcodeinstを更新できること、また既存のinvoke
ポインタを無効にすることなくcodeinstを更新できることを保証します。これにより、JITは将来的により高い最適化レベルで関数を再最適化する可能性があり、また将来的には関数の同時コンパイルをサポートすることができます。
- tdpThis is not a totally-target independent pipeline, as transformations such as vectorization rely upon target information such as vector register width and cost modeling. Additionally, codegen itself makes a few target-dependent assumptions, and the optimization pipeline will take advantage of that knowledge.