24

簡単にするために、このシナリオを想像してください。r1 と r2 と呼ばれる 2 ビット レジスタのペアがあり、即時アドレス指定でのみ動作する 2 ビット コンピュータがあります。

ビットシーケンス00が CPU への追加を意味するとしましょう。また、01はデータを r1 に移動することを意味し、 10はデータを r2 に移動することを意味します。

したがって、このコンピューター用のアセンブリ言語とアセンブラーがあり、サンプル コードは次のように記述されます。

mov r1,1
mov r2,2
add r1,r2

簡単に言えば、このコードをネイティブ言語にアセンブルすると、ファイルは次のようになります。

0101 1010 0001

上記の 12 ビットは、次のネイティブ コードです。

Put decimal 1 to R1, Put decimal 2 to R2, Add the data and store in R1. 

これがコンパイルされたコードの基本的な仕組みですよね?

誰かがこのアーキテクチャ用の JVM を実装するとしましょう。Java では、次のようなコードを記述します。

int x = 1 + 2;

JVM はこのコードをどの程度正確に解釈しますか? つまり、最終的には同じビット パターンを CPU に渡さなければならないということですね。すべての CPU には、理解して実行できる多くの命令があり、それらは結局のところほんの一部です。コンパイルされた Java バイトコードが次のようになっているとします。

1111 1100 1001

または何でも..解釈すると、実行時にこのコードが0101 1010 0001に変更されるということですか?もしそうなら、それはすでにネイティブ コードに含まれているのに、なぜ JIT は何度か実行された後にしか機能しないと言われているのでしょうか? 正確に 0101 1010 0001 に変換されない場合、どうなりますか? どのようにしてCPUに加算を行わせるのですか?

たぶん、私の仮定にはいくつかの間違いがあります。

解釈が遅く、コンパイルされたコードは高速ですが、移植性がなく、仮想マシンがコードを「解釈」することはわかっていますが、どのように? 「正確に/技術的に解釈する方法」が行われていることを探しています。回答の代わりに、任意のポインター (書籍や Web ページなど) も歓迎します。

4

4 に答える 4

21

残念ながら、あなたが説明するCPUアーキテクチャは制限が多すぎて、すべての中間ステップでこれを明確にすることはできません. 代わりに、疑似 C と疑似 x86 アセンブラを書きます。うまくいけば、C や x86 にあまり詳しくなくてもわかりやすい方法で書きます。

コンパイルされた JVM バイトコードは次のようになります。

ldc 0 # push first first constant (== 1)
ldc 1 # push the second constant (== 2)
iadd # pop two integers and push their sum
istore_0 # pop result and store in local variable

インタプリタは、配列内のこれらの命令 (のバイナリ エンコード) と、現在の命令を参照するインデックスを持っています。また、定数の配列、スタックとして使用されるメモリ領域、およびローカル変数用のメモリ領域もあります。次に、インタプリタ ループは次のようになります。

while (true) {
    switch(instructions[pc]) {
    case LDC:
        sp += 1; // make space for constant
        stack[sp] = constants[instructions[pc+1]];
        pc += 2; // two-byte instruction
    case IADD:
        stack[sp-1] += stack[sp]; // add to first operand
        sp -= 1; // pop other operand
        pc += 1; // one-byte instruction
    case ISTORE_0:
        locals[0] = stack[sp];
        sp -= 1; // pop
        pc += 1; // one-byte instruction
    // ... other cases ...
    }
}

このC コードは、マシン コードにコンパイルされて実行されます。ご覧のとおり、非常に動的です。命令が実行されるたびに各バイトコード命令を検査し、すべての値がスタック (つまり RAM) を通過します。

実際の追加自体はおそらくレジスターで行われますが、追加を取り巻くコードは、Java からマシンへのコード コンパイラーが出力するものとはかなり異なります。以下は、C コンパイラが上記を (pseudo-x86) に変換する可能性があるものからの抜粋です。

.ldc:
incl %esi # increment the variable pc, first half of pc += 2;
movb %ecx, program(%esi) # load byte after instruction
movl %eax, constants(,%ebx,4) # load constant from pool
incl %edi # increment sp
movl %eax, stack(,%edi,4) # write constant onto stack
incl %esi # other half of pc += 2
jmp .EndOfSwitch

.addi
movl %eax, stack(,%edi,4) # load first operand
decl %edi # sp -= 1;
addl stack(,%edi,4), %eax # add
incl %esi # pc += 1;
jmp .EndOfSwitch

追加のオペランドは、Java プログラムの目的では定数ですが、ハードコードされているのではなく、メモリから取得されていることがわかります。これは、インタープリターにとって、それらが一定ではないためです。インタプリタは、一度コンパイルされると、特殊なコードを生成することなく、あらゆる種類のプログラムを実行できなければなりません。

JIT コンパイラの目的は、まさにそれを行うことです。特殊なコードを生成します。JIT は、スタックを使用してデータを転送する方法、プログラム内のさまざまな定数の実際の値、および実行される一連の計算を分析して、同じことをより効率的に行うコードを生成できます。サンプル プログラムでは、ローカル変数 0 をレジスタに割り当て、定数テーブルへのアクセスをレジスタへの定数の移動に置き換え ( movl %eax, $1)、スタック アクセスを適切なマシン レジスタにリダイレクトします。通常行われるいくつかの最適化 (コピーの伝播、定数の折りたたみ、デッド コードの削除) を無視すると、次のようなコードになる可能性があります。

movl %ebx, $1 # ldc 0
movl %ecx, $2 # ldc 1
movl %eax, %ebx # (1/2) addi
addl %eax, %ecx # (2/2) addi
# no istore_0, local variable 0 == %eax, so we're done
于 2015-01-25T10:45:54.227 に答える
2

Java での重要な手順の 1 つは、コンパイラが最初にコードを Java バイトコードを含むファイルに変換すること.javaです.classファイルを取得して、この中間言語.classを理解する任意のマシンで実行し、その場で行ごと、またはチャンクごとに翻訳できるため、これは便利です。これは、Java コンパイラー + インタープリターの最も重要な機能の 1 つです。できますJava ソース コードをネイティブ バイナリに直接コンパイルしますが、これは元のコードを一度記述すればどこでも実行できるという考えを無効にします。これは、コンパイルされたネイティブ バイナリ コードが、コンパイルされたのと同じハードウェア/OS アーキテクチャでのみ実行されるためです。別のアーキテクチャで実行したい場合は、そのアーキテクチャでソースを再コンパイルする必要があります。中間レベルのバイトコードへのコンパイルでは、ソース コードではなくバイトコードをドラッグする必要があります。バイトコードを解釈して実行できる JVM が必要になるため、これは別の問題です。そのため、インタープリターが実行する中間レベルのバイトコードへのコンパイルは、プロセスの不可欠な部分です。

コードの実際のリアルタイム実行に関して: はい、JVM は最終的に、ネイティブにコンパイルされたコードと同一である場合と同一でない場合があるバイナリ コードを解釈/実行します。1 行の例では、表面的には同じように見えるかもしれません。ただし、解釈は通常、すべてをプリコンパイルするわけではありませんが、バイトコードを調べて、行単位またはチャンク単位のバイナリに変換します。これには (C や C コンパイラなどのネイティブにコンパイルされたコードと比較して) 長所と短所があり、さらに詳しく読むためのオンラインのリソースがたくさんあります。ここ、またはthis、またはthis oneの私の答えを参照してください。

于 2015-01-25T10:24:42.070 に答える
2

すべてのコンピューターが同じ命令セットを持っているわけではありません。Java バイトコードは、通信を改善するための人工言語であるエスペラント語の一種です。Java VM は、ユニバーサル Java バイトコードを、それが実行されているコンピューターの命令セットに変換します。

では、JIT はここでどのように機能するのでしょうか。JIT コンパイラの主な目的は最適化です。多くの場合、特定のバイトコードをターゲットのマシン コードに変換するには、さまざまな方法があります。最もパフォーマンスが理想的な変換は、データに依存する可能性があるため、多くの場合自明ではありません。また、プログラムがアルゴリズムを実行せずに解析できる範囲にも制限があります。停止問題はよく知られている制限ですが、それだけではありません。したがって、JIT コンパイラーが行うことは、可能なさまざまな変換を試し、プログラムが処理する実際のデータを使用してそれらがどれだけ速く実行されるかを測定することです。したがって、JIT コンパイラーが完全な翻訳を見つけるまでには、何度も実行する必要があります。

于 2015-01-25T10:27:48.877 に答える