Eval of Julia code
Julia言語がコードを実行する仕組みを学ぶ上で最も難しい部分の一つは、コードのブロックを実行するためにすべての要素がどのように連携しているかを学ぶことです。
各コードのチャンクは、通常、次のような潜在的に馴染みのない名前を持つ多くのステップを経ます(順不同):flisp、AST、C++、LLVM、eval
、typeinf
、macroexpand
、sysimg(またはシステムイメージ)、ブートストラップ、コンパイル、パース、実行、JIT、インタープリタ、ボックス、アンボックス、内蔵関数、プリミティブ関数、そして最終的に望ましい結果(うまくいけば)に変わります。
Julia Execution
全体プロセスの10,000フィートの概要は次のとおりです:
- ユーザーは
julia
を開始します。 cli/loader_exe.c
の C 関数main()
が呼び出されます。この関数はコマンドライン引数を処理し、jl_options
構造体を埋め、変数ARGS
を設定します。その後、Julia を初期化します(julia_init
ininit.c
を呼び出すことによって)、以前にコンパイルされた sysimg をロードする可能性があります。最後に、Base._start()
を呼び出すことによって、制御を Julia に渡します。_start()
が制御を引き継ぐと、その後のコマンドのシーケンスは与えられたコマンドライン引数に依存します。たとえば、ファイル名が指定されている場合、それを実行します。そうでなければ、インタラクティブなREPLを開始します。- ユーザーとのREPLのインタラクションに関する詳細は省略しますが、プログラムは実行したいコードのブロックを持つことになります。
- コードを実行するブロックがファイルにある場合、
jl_load(char *filename)
がファイルをロードするために呼び出され、parse がそれを実行します。各コードの断片はeval
に渡されて実行されます。 - 各コードの断片(またはAST)は、
eval()
に渡され、結果に変換されます。 eval()
は各コードフラグメントを取り出し、jl_toplevel_eval_flex()
で実行しようとします。jl_toplevel_eval_flex()
は、コードが関数内では無効な「トップレベル」アクション(using
やmodule
など)であるかどうかを判断します。そうであれば、コードをトップレベルインタープリタに渡します。jl_toplevel_eval_flex()
その後 expands マクロを排除し、ASTを「単純化」して実行しやすくするためのコード。jl_toplevel_eval_flex()
は、ASTをJITコンパイルするか、直接解釈するかを決定するために、いくつかの単純なヒューリスティックを使用します。- 作業の大部分は、
eval
ininterpreter.c
のコードを解釈するために処理されます。 - もし代わりにコードがコンパイルされると、作業の大部分は
codegen.cpp
によって処理されます。特定の引数の型のセットで初めて Julia 関数が呼び出されると、type inference がその関数で実行されます。この情報は、より高速なコードを生成するために codegen ステップによって使用されます。 - 最終的に、ユーザーはREPLを終了するか、プログラムの終わりに達し、
_start()
メソッドが戻ります。 main()
が終了する直前に、jl_atexit_hook(exit_code)
を呼び出します。 これにより、Base._atexit()
が呼び出され(これは、Julia内でatexit()
に登録された関数を呼び出します)、次にjl_gc_run_all_finalizers()
を呼び出します。 最後に、すべてのlibuv
ハンドルを適切にクリーンアップし、それらがフラッシュして閉じるのを待ちます。
Parsing
ジュリアパーサーは、femtolispで書かれた小さなlispプログラムで、そのソースコードはジュリア内のsrc/flispに配布されています。
このインターフェース機能は主に jlfrontend.scm
で定義されています。 ast.c
のコードは、Julia 側でこのハンドオフを処理します。
この段階での他の関連ファイルは julia-parser.scm
で、これはJuliaコードをトークン化し、ASTに変換する処理を行います。また、julia-syntax.scm
は、複雑なAST表現をより単純な「低下」AST表現に変換する処理を行い、分析や実行により適したものにします。
フロントエンドを単独で実行することで、Juliaを完全に再構築することなくパーサーをテストしたい場合は、次のように実行できます:
$ cd src
$ flisp/flisp
> (load "jlfrontend.scm")
> (jl-parse-file "<filename>")
Macro Expansion
eval()
がマクロに遭遇すると、そのASTノードを展開してから式の評価を試みます。マクロ展開は、4d61726b646f776e2e436f64652822222c20226576616c28292229_40726566
(Juliaで)から、パーサ関数 jl_macroexpand()
(flisp
で書かれた)を経て、Juliaマクロ自体(他に何があるのか - Juliaで書かれた)に fl_invoke_julia_macro()
を介して渡し、再び戻るというプロセスを含みます。
通常、マクロ展開は Meta.lower()
/jl_expand()
への呼び出しの最初のステップとして呼び出されますが、macroexpand()
/jl_macroexpand()
への呼び出しによって直接呼び出すこともできます。
Type Inference
型推論は、typeinf()
in compiler/typeinfer.jl
によって実装されています。型推論は、Julia関数を調べ、その変数の各々の型の境界を決定し、関数からの戻り値の型の境界を決定するプロセスです。これにより、既知の不変値のアンボックス化や、フィールドオフセットや関数ポインタの計算など、さまざまなランタイム操作のコンパイル時のホイストなど、将来の多くの最適化が可能になります。型推論には、定数伝播やインライン化などの他のステップも含まれる場合があります。
JIT Code Generation
Codegenは、JuliaのASTをネイティブマシンコードに変換するプロセスです。
JIT環境は、jl_init_codegen
in codegen.cpp
への早期呼び出しによって初期化されます。
要求に応じて、Juliaメソッドはemit_function(jl_method_instance_t*)
関数によってネイティブ関数に変換されます。(注:MCJIT(LLVM v3.4+を使用)を使用する場合、各関数は新しいモジュールにJITされなければなりません。)この関数は、関数全体が出力されるまで再帰的にemit_expr()
を呼び出します。
このファイルの残りの大部分は、特定のコードパターンのさまざまな手動最適化に専念しています。たとえば、emit_known_call()
は、さまざまな引数タイプの組み合わせに対して多くのプリミティブ関数をインライン化する方法を知っています(builtins.c
)。
コード生成の他の部分は、さまざまなヘルパーファイルによって処理されます:
JIT関数のバックトレースを処理します
ccall
とllvmcall
FFI、およびさまざまなabi_*.cpp
ファイルを処理します。さまざまな低レベルの内在関数の発行を処理します
System Image
システムイメージは、一連のJuliaファイルのプリコンパイルされたアーカイブです。Juliaに付属するsys.ji
ファイルは、そのようなシステムイメージの一例であり、ファイルsysimg.jl
を実行することによって生成され、結果として得られた環境(タイプ、関数、モジュール、および定義されたすべての値を含む)をファイルにシリアライズします。したがって、これはMain
、Core
、およびBase
モジュールのフローズンバージョン(およびブートストラップの最後に環境にあったその他のもの)を含んでいます。このシリアライザー/デシリアライザーは、jl_save_system_image
/jl_restore_system_image
in staticdata.c
によって実装されています。
もしsysimgファイルが存在しない場合(jl_options.image_file == NULL
)、これはコマンドラインで--build
が指定されたことを意味しますので、最終的な結果は新しいsysimgファイルになります。Juliaの初期化中に、最小限のCore
およびMain
モジュールが作成されます。その後、現在のディレクトリからboot.jl
という名前のファイルが評価されます。Juliaはコマンドライン引数として与えられたファイルを評価し続け、最後に達します。最後に、結果の環境を将来のJulia実行の出発点として使用するために「sysimg」ファイルに保存します。