バイトコード対マシンコードへのコンパイル


12

(Javaのように)中間のバイトコードを生成するコンパイルは、マシンコードまで「ずっと」進むのではなく、一般に複雑さが少なくなります(したがって、時間がかかります)。

回答:


21

はい、Javaバイトコードへのコンパイルは、マシンコードへのコンパイルよりも簡単です。これは、JVMが実際のCPUよりもはるかに単純なマシンであり、プログラミングに便利であるという理由もあります。 Java言語と連携して、ほとんどのJava操作は非常に簡単な方法で1つのバイトコード操作にマッピングされます。別の非常に重要な理由は、実際にはないということです最適化が行われます。効率に関するほとんどすべての問題はJITコンパイラー(またはJVM全体)に委ねられているため、通常のコンパイラーの中間部分はすべてなくなります。基本的にASTを1回通り抜けて、各ノードの既製のバイトコードシーケンスを生成できます。メソッドテーブル、定数プールなどを生成する「管理オーバーヘッド」がいくつかありますが、LLVMなどの複雑さに比べれば何もありません。


「...... middle end of ...」と書きました。「...中位から終わりまで...」という意味ですか?または、「...の中間部分」ですか?
ジュリアンA.

5
@Julianの「ミドルエンド」は、セマンティクスを考慮せずに「フロントエンド」および「バックエンド」と同様に造語された実際の用語です:)

7

コンパイラは、人間が読める1テキストファイルを取得し、それらをマシン用のバイナリ命令に変換するプログラムです。一歩下がって、この理論的な観点から質問について考えてみると、複雑さはほぼ同じです。ただし、より実用的なレベルでは、バイトコードコンパイラの方が簡単です。

プログラムをコンパイルするには、どのような広範な手順が必要ですか?

  1. ソースコードのスキャン、解析、検証。
  2. ソースを抽象構文ツリーに変換します。
  3. オプション:言語仕様で許可されている場合は、ASTを処理および改善します(デッドコードの削除、操作の並べ替え、その他の最適化)
  4. ASTを、マシンが理解できる形式に変換します。

2つの間に2つの本当の違いがあります。

  • 一般に、複数のコンパイル単位を持つプログラムは、マシンコードにコンパイルするときにリンクする必要があり、通常はバイトコードではリンクしません。リンクがこの質問の文脈でコンパイルの一部であるかどうかについて、髪を分けることができます。もしそうなら、バイトコードのコンパイルは少し簡単になります。ただし、リンクの複雑さは、多くのリンクの問題がVMによって処理される実行時に補われます(以下のメモを参照)。

  • バイトコードコンパイラは、VMがその場でより適切に実行できるため、あまり最適化しない傾向があります(最近では、JITコンパイラはかなり標準的なVMへの追加です)。

このことから、バイトコードコンパイラはほとんどの最適化とすべてのリンクの複雑さを省略し、これらの両方をVMランタイムに委ねることができると結論付けました。バイトコードコンパイラは、マシンコードコンパイラがそれ自体で行う多くの複雑さをVMにシャベルでかけるため、実際にはより単純です。

1 難解な言語は数えない


3
最適化などを無視するのはばかげています。これらの「オプションの手順」は、ほとんどのコンパイラのコードベース、複雑さ、およびコンパイル時間の大部分を占めます。

実際には、それは正しいです。私はここで学問を練っていたので、答えを更新しました。

最適化を実際に禁止する言語仕様はありますか?一部の言語はそれを難し​​くしているが、最初から許可されていないことを理解していますか?
Davidmh

@Davidmh私はそれらを禁止する仕様を知りません。私の理解では、ほとんどの人はコンパイラが許可されていると言っていますが、詳細には触れません。多くの最適化は一般にCPU、OS、およびターゲットアーキテクチャの詳細に依存するため、各実装は異なります。このため、バイトコードコンパイラは最適化する可能性が低く、代わりに、基盤となるアーキテクチャを知っているVMにそれをパントします。

4

コンパイルは常にJavaから汎用仮想マシンコードであるため、コンパイラの設計が簡素化されると思います。また、コードを一度だけコンパイルするだけで、(各マシンでコンパイルする代わりに)任意のプラットフォームで実行できます。仮想マシンを標準化されたマシンのように考えることができるため、コンパイル時間が短くなるかどうかはわかりません。

一方、各マシンは「バイトコード」(これはJavaコードのコンパイルから生じる仮想マシンコード)を解釈し、それを実際のマシンコードに変換して実行できるように、Java仮想マシンをロードする必要があります。

Imoこれは非常に大きなプログラムには適していますが、小さなプログラムには非常に悪いです(仮想マシンはメモリの浪費であるため)。


そうですか。ですから、バイトコードを標準マシン(つまりJVM)にマッピングする複雑さは、ソースコードを物理マシンにマッピングする複雑さと一致し、バイトコードがコンパイル時間を短縮すると考える理由を残さないと思いますか?
ジュリアンA.

それは私が言ったことではありません。Javaコードをバイトコード(仮想マシンアセンブラー)にマッピングすると、ソースコード(Java)を物理マシンコードにマッピングする場合と一致すると述べました。
マンドリル

3

コンパイルの複雑さは、ソース言語とターゲット言語のセマンティックギャップ、およびこのギャップを埋めるときに適用する最適化のレベルに大きく依存します。

たとえば、JavaソースコードをJVMバイトコードにコンパイルするのは比較的簡単です。これは、JavaのコアサブセットがJVMバイトコードのサブセットにほとんど直接マッピングされるためです。JavaにはループがありますがGOTO、JVMにはGOTOループがありませんが、Javaにはジェネリックがありますが、JVMにはありませんが、それらは簡単に処理できます(ループから条件付きジャンプへの変換は簡単で、型消去はわずかに少ないです)ので、まだ管理可能です)。他にも違いはありますが、それほど深刻ではありません。

JVMバイトコードにコンパイルRubyのソースコードを、より多くの(特に前関与しているinvokedynamicMethodHandlesJavaの7に導入された、あるいはより正確には、JVM仕様の第3版では)。Rubyでは、メソッドは実行時に置き換えることができます。JVMでは、実行時に置き換えることができるコードの最小単位はクラスであるため、RubyメソッドはJVMメソッドではなくJVMクラスにコンパイルする必要があります。RubyメソッドのディスパッチはJVMメソッドのディスパッチと一致せず、以前はinvokedynamic、独自のメソッドディスパッチメカニズムをJVMに注入する方法がありませんでした。Rubyには継続とコルーチンがありますが、JVMにはそれらを実装する機能がありません。(JVMのGOTO メソッド内のジャンプターゲットに制限されています。)JVMが持っている唯一の制御フロープリミティブは、継続を実装するのに十分強力であり、コルーチンスレッドを実装するのに非常に強力ですが、コルーチンの全体的な目的は非常に軽量である。

RubyのソースコードをRubiniusバイトコードまたはYARVバイトコードにコンパイルするOTOHは、どちらもRubyのコンパイルターゲットとして明示的に設計されているので、ささいなことです(RubiniusはCoffeeScriptなどの最も有名なファンシーにも使用されていますが) 。

同様に、x86ネイティブコードをJVMバイトコードにコンパイルするのは簡単ではありません。この場合も、かなり大きなセマンティックギャップがあります。

Haskellにはもう1つの良い例があります。Haskellには、ネイティブx86マシンコードを生成する、高性能で産業用の強力なプロダクション対応コンパイラがいくつかありますが、現在まで、JVMまたはCLIギャップは非常に大きいため、それを埋めるのは非常に複雑です。したがって、これは、ネイティブマシンコードへのコンパイルが、JVMまたはCILバイトコードへのコンパイルよりも実際に複雑でない例です。これは、ネイティブマシンコードにははるかに低いレベルのプリミティブ(GOTO、ポインターなど)があり、メソッド呼び出しや例外などのより高いレベルのプリミティブを使用するよりも簡単に「強制」したいためです。

そのため、ターゲット言語のレベルが高いほど、コンパイラの複雑さを軽減するためにソース言語のセマンティクスとより厳密に一致する必要があると言えます。


0

実際、今日のほとんどのJVMは非常に複雑なソフトウェアであり、JITコンパイルを実行しています(したがって、バイトコードはJVMによってマシンコードに動的に変換されます)。

そのため、Javaソースコード(またはClojureソースコード)からJVMバイトコードへのコンパイルは実際には簡単ですが、JVM自体はマシンコードへの複雑な変換を行っています。

JVM内のこのJIT変換が動的であるという事実により、JVMはバイトコードの最も関連性のある部分に集中できます。実際には、ほとんどのJVMは、JVMバイトコードの最もホットな部分(たとえば、最も呼び出されたメソッド、または最も実行された基本ブロック)をより最適化します。

JVM + Javaからバイトコードへのコンパイラーの複雑さの合計が、事前処理コンパイラーの複雑さよりも大幅に少ないかどうかはわかりません。

(のようなほとんどの伝統的なコンパイラという通知もGCC又はクラン/ LLVMが内部表現(に)入力C形質転換された(またはC ++、またはエイダを、...)ソースコードGIMPLE GCC用LLVM全く同様であるクラン用)バイトコード。次に、その内部表現(最初にそれを最適化します。つまり、ほとんどのGCC最適化パスは、入力としてGimpleを取得し、出力としてGimpleを生成します。後でアセンブラーまたはマシンコードを生成します)。

ところで、最近のGCC(特にlibgccjit)およびLLVMインフラストラクチャでは、それらを使用して他の(または独自の)言語を内部GimpleまたはLLVM表現にコンパイルし、その後、ミドルエンドおよびバックエンドの多くの最適化機能から利益を得ることができます。これらのコンパイラの最後の部分。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.