「Javaのように」暫定的なバイトコードを生成するコンパイルは、マシンコードに「完全に」進むのではなく、一般的に複雑さが軽減されます(したがって、時間がかかりません)。
はい、Javaバイトコードへのコンパイルは、マシンコードへのコンパイルよりも簡単です。これは、ターゲットにするフォーマットが1つしかないためです(Mandrillが言及しているように、これはコンパイラの複雑さを軽減するだけで、コンパイル時間ではありません)。 JVMは、実際のCPUよりもはるかに単純なマシンであり、プログラミングに便利であるためです。これは、Java言語、ほとんどのJava操作と連携して設計されているためです。非常に単純な方法で正確に1つのバイトコード操作にマップします。もう1つの非常に重要な理由は、実際にはno最適化が行われることです。ほとんどすべての効率の問題は、JITコンパイラー(またはJVM全体)に任されています。 、そのため、通常のコンパイラの中間全体が消えます。基本的にASTを1回実行して、ノードごとに既製のバイトコードシーケンスを生成できます。メソッドテーブルの生成には「管理上のオーバーヘッド」があります。 、定数プールなどですが、たとえばLLVMの複雑さと比較すると、それは何もありません。
コンパイラは単に人間が読める形式のプログラムです1 テキストファイルを作成し、それらをマシンのバイナリ命令に変換します。一歩下がって、この理論的な観点から質問について考える場合、複雑さはほぼ同じです。ただし、より実用的なレベルでは、バイトコードコンパイラの方が簡単です。
プログラムをコンパイルするには、どのような広範な手順が必要ですか?
2つの間に2つの実際の違いのみがあります。
一般に、複数のコンパイル単位を持つプログラムは、マシンコードにコンパイルするときにリンクを必要とし、通常はバイトコードを使用しません。リンクがこの質問のコンテキストでのコンパイルの一部であるかどうかについて、髪の毛を分けることができます。もしそうなら、バイトコードのコンパイルは少し簡単になります。ただし、リンクの複雑さは、VM(下記の私のメモを参照)によって処理される多くのリンクの懸念事項が実行時に実行時に補われます)。
VMはオンザフライでこれをよりよく実行できるため、バイトコードコンパイラはそれほど最適化しない傾向があります(JITコンパイラは、最近のVMへのかなり標準的な追加です)。
このことから、バイトコードコンパイラは、ほとんどの最適化とすべてのリンクの複雑さを省略でき、これらの両方をVMランタイムに延期します。バイトコードコンパイラは、多くの複雑さを示しているため、実際にはより単純です。 VM=マシンコードコンパイラがそれ自体を引き受ける。
1数えない 難解な言語
コンパイルは常にJava汎用仮想マシンコードに対して)であるため、コンパイラー設計を単純化すると言えます。これは、コードを一度コンパイルするだけで、任意のプラットフォームで実行できることを意味します(代わりに各マシンでコンパイルする必要があります。仮想マシンは標準化されたマシンと同じように考えることができるため、コンパイル時間が短くなるかどうかはわかりません。
一方、各マシンには「Java仮想マシンがロードされているため、「バイトコード」(Javaコードのコンパイル)、それを実際のマシンコードに変換して実行します。
Imoこれは非常に大きなプログラムには適していますが、小さなプログラムには非常に悪いです(仮想マシンはメモリの浪費であるため)。
コンパイルの複雑さは、ソース言語とターゲット言語の間のセマンティックギャップと、このギャップを埋めるときに適用する最適化のレベルに大きく依存します。
たとえば、JavaソースコードをJVMバイトコードにコンパイルすることは比較的簡単です。これは、Javaのコアサブセットがサブセットに直接マッピングされるためです。 JavaループがあるがGOTO
がない、JVMはGOTO
があるがループがない、Javaにはジェネリックがありますが、JVMにはありませんが、それらは簡単に処理できます(ループから条件付きジャンプへの変換は簡単で、型の消去はやや少なくなりますが、それでも管理可能です)。
RubyソースコードからJVMバイトコードへのコンパイルは、特にinvokedynamic
およびMethodHandles
がJava = 7、またはより正確には第3版のJVM仕様。Rubyでは、メソッドは実行時に置き換えることができます。JVMでは、実行時に置き換えることができるコードの最小単位はクラスなので、Rubyメソッドは、JVMメソッドではなくJVMクラスにコンパイルする必要があります。RubyメソッドディスパッチがJVMメソッドディスパッチと一致せず、invokedynamic
の前に、独自のメソッドディスパッチメカニズムをJVMに挿入する方法はありません。Rubyには継続とコルーチンがありますが、JVMにはそれらを実装する機能がありません。(JVMのGOTO
は、メソッド内のジャンプターゲット。)JVMが持っている唯一の制御フロープリミティブ。継続を実装するのに十分強力な例外であり、コルーチンスレッドを実装します。どちらも非常に重いですが、コルーチンの全体的な目的はveです。軽量です。
OTOH、コンパイルRuby RubiniusバイトコードまたはYARVバイトコードへのソースコードは、両方ともRuby( Rubiniusは、CoffeeScriptなどのその他の言語にも使用されていますが、最も有名なのはFancyです)。
同様に、x86ネイティブコードをJVMバイトコードにコンパイルすることは簡単ではありません。ここでも、かなり大きなセマンティックギャップがあります。
Haskellは、もう1つの良い例です。Haskellには、ネイティブのx86マシンコードを生成する高性能で産業用の強力なコンパイラーがいくつかありますが、現時点では、JVMとCLIのどちらにも機能するコンパイラーはありません。ギャップは非常に大きく、それを埋めるのは非常に複雑です。したがって、これは、ネイティブマシンコードへのコンパイルが、JVMまたはCILバイトコードへのコンパイルよりも実際にless複雑な例です。これは、ネイティブマシンのコードに、より低いレベルのプリミティブ(GOTO
、ポインターなど)があり、メソッド呼び出しや例外などのより高いレベルのプリミティブを使用するよりも簡単に「強制」できるためです。
したがって、ターゲット言語のレベルが高いほど、コンパイラーの複雑さを軽減するために、ソース言語のセマンティクスとより厳密に一致させる必要があると言えます。
実際には、今日のほとんどのJVMは非常に複雑なソフトウェアであり、 JITコンパイル (したがって、バイトコードはdynamicallyによってマシンコードに変換されますJVM)。
したがって、Javaソースコード(またはClojureソースコード)からJVMバイトコードへのコンパイルは確かに簡単ですが、JVM自体はマシンコードへの複雑な変換を行っています。
JVM内のこのJIT変換は動的であるため、JVMはバイトコードの最も関連性の高い部分に焦点を合わせることができます。実際には、ほとんどのJVMは、JVMバイトコードの最もホットな部分(最も呼び出されたメソッド、または最も実行された基本ブロックなど)を最適化します。
JVMの組み合わせ複雑さ+ Javaからバイトコードコンパイラへの複雑さは、事前コンパイラ。
ほとんどの従来のコンパイラー( [〜#〜] gcc [〜#〜] または Clang/LLVM など)が入力C(またはC++、またはAda、 ...)ソースコードを内部表現に変換( Gimple はGCC、 [〜#〜] llvm [〜#〜] はClang)いくつかのバイトコードに非常に似ています。次に、その内部表現を最初にそれ自体に最適化します。つまり、ほとんどのGCC最適化パスは、Gimpleを入力として取得し、Gimpleを出力として生成します。その後、Gimpleからアセンブラーまたはマシンコードを発行して、オブジェクトコードに変換します。
ところで、最近のGCC(特に libgccjit )とLLVMインフラストラクチャを使用すると、それらを使用して他の(または独自の)言語を内部GimpleまたはLLVM表現にコンパイルし、多くの最適化機能から利益を得ることができますこれらのコンパイラのミドルエンドとバックエンドの部分。