JVMのJIT
8593 ワード
JIT技術はJVMの中で最も重要なコアモジュールの一つである.私の授業ではもともとこの記事を計画していませんでしたが、Javaはいったいどうやって運営されているのかという友人が絶えずいたので、HotspotがC++で書かれている以上、JavaはC++の上で実行されていると言えるのではないでしょうか.これらの概念を明らかにするために、私はこのような文章を加えたことを思い出して、番外編にしましょう.
Just In Time
Just in timeコンパイルは、ランタイムコンパイルとも呼ばれ、C/C++言語とは異なり直接機械命令に翻訳され、javacはjavaのソースファイルをclassファイルに翻訳し、classファイルはすべてJavaバイトコードである.では、JVMはこれらのclassファイルをロードした後、これらのバイトコードに対して、1つずつ取り出し、1つずつ実行する方法であり、この方法は実行を説明することである.
もう1つは,これらのJavaバイトコードを再コンパイル最適化し,マシンコードを生成し,CPUに直接実行させることである.このようにして作成されたコードはより効率的になります.通常,すべてのJavaメソッドをマシンコードにコンパイルする必要はなく,最も頻繁に呼び出され,CPUを占有する時間が最も長いメソッドを探し出してマシンコードにコンパイルするだけである.この呼び出しが最も頻繁なJavaメソッドは、私たちがよく言うホットスポットメソッド(Hotspot、この仮想マシンの名前がここから来たとは限らない)です.
このような実行時に必要に応じてコンパイルする方法がJust In Timeである.
主な技術点
実はJITの主な技術点は、大きなフレームワークから言えば、とても簡単で、書く権限と実行権限のあるメモリを申請して、それからあなたがコンパイルするJavaの方法を、マシンコードに翻訳して、このメモリに書きます.元のJavaメソッドを呼び出す必要がある場合は、このメモリを呼び出すに移ります.
例を見てみましょう
上の例は簡単です.3に1を加えて印刷します.次のコマンドで、マシンコードを確認します.
そして、この出力の山では、incメソッドが最終的にこのようなマシンコードに翻訳されることがわかります.
説明します(読者は一定のx 86アセンブリ言語の知識が必要です).
最初の文では、前のスタックフレームのベースアドレスを保存し、現在のスタックポインタをスタックベースレジスタに割り当てます.これは、関数に入る通常の動作です.私たちはそれを気にしない.
第三句、ediをスタックに保存します.x 64プロセッサでは、最初の6つのパラメータはレジスタを使用してパラメータを転送します.最初のパラメータはrdi、2番目のパラメータはrsiなどを使用します.だからediに保存されているのは実は最初のパラメータ、つまり整数3で、なぜrdiの32ビット低い、つまりediを使うのでしょうか.私たちの入参aはint型ですからね.long型に変えて効果を見ることができます.
4番目の文は、前のステップでスタックに保存した整数をeaxに保存します.
5文目以降、eaxに1を加えて、スタックを退き、戻ります.x 64の規定(ABI)に従って、戻り値はeaxを介して伝達される.
私たちは見て、実は第3文、第4文はまったく存在する必要がないようで、gccのデフォルトの情況の下で、生成したマシンコードは少し馬鹿で、それはいつも入参をスタックの上に置くことができて、しかし実は、私たちは直接rdiの中からraxの中にパラメータを入れることができます.気に入らない.では、自分で直して、もっと簡素にすることができます.どうしますか.答えは実行時にincの論理を修正することです.
この例では、mmapを使用して書き込み権限と実行権限のあるメモリを申請し、手書きのマシンコードをコピーし、関数ポインタを使用してメモリを指し、呼び出します.このようにして、この手書きのマシンコードを実行することができます.
実行してみます.
この過程をもう一度思い出してください.私たちは手書きマシンコードで元のinc関数を置き換えた.新しい例では,プログラムで定義したデータを用いてinc関数を再構築した.このように実行中のプロセスで新しい関数を作成する方法がJITのコア操作である.
インタプリタ,C 1とC 2
Hotspotでは,インタプリタはバイトコードごとに小さなマシンコードを生成し,Javaメソッドを実行する過程で命令を1つずつ取り,その命令に対応するマシンコードを実行する.256個の命令は、テーブルを構成し、このテーブルでは、各命令がマシンコードに対応し、ある命令を実行すると、このテーブルからこのマシンコードを調べ、jmp命令によってこのマシンコードを実行すればよい.
この方式をテンプレート解釈器と呼ぶ.
テンプレート解釈器が生成するコードには、上記の最初の例のように冗長性がたくさんあります.より簡素なマシンコードを生成するために,グローバル値符号化,デッドコード除去,スカラー展開,共通サブ表現除去,定数伝搬などのコンパイラ最適化手段を導入することができる.このようにして生成されたマシンコードはより最適化されます.
しかし,マシンコードを生成する品質が高いほど,所要時間が長くなる.JITスレッドもJavaアプリケーションスレッドのリソースを絞り込む.だからC 1はトレードオフで、コンパイル時間はあまり長くなく、生成されたマシンコードの命令も最適化されていませんが、解釈器よりも効率が高いに違いありません.
Javaメソッドが十分に頻繁に呼び出されると、より良質なマシンコードを生成するのに苦労する価値があります.このとき、C 2コンパイルがトリガーされ、c 2はより遅く動作しますが、より効率的なコードを生成できるコンパイラです.
このことから,実際にJavaの動作は,ほとんどが実行時に生成されたマシンコードに依存していることが分かる.だから、冒頭の質問に「JavaはC++で実行されていますか?」という質問には、みんな自分の答えがあるはずです.この質問は簡単には答えられませんが、正しい答えはJavaの実行依存テンプレート解釈器とJITコンパイラです.
もっと最適化して
私たちのこの授業で挙げた例では、inc関数に入ってからスタックを全く使用していない以上、実際にはスタックフレームを開く必要はありません.だからpush rbp,pop rbpの論理をすべて取り除くことができます.
次のように最適化されます.
命令がさらに簡素化されていることがわかります.再コンパイルして実行するか、8を印刷することに成功しました.
この問題によると、なぜleaは計算に使われるのですか.
さらに最適化されたコードを書くこともできます.
gccの最適化コンパイルを開くと、例えば、この方法に対しても、このようなコードを得ることができます.
-O 2最適化の使用:
incのマシンコードがこうなりました
これは私たちが手書きで最適化したマシンコードと全く同じです.
実際,C 1とC 2が行うべきことはgccの最適化コンパイルと同様であり,特定の方法を用いてより効率的なマシンコードを生成することである.しかし、原理的には、運転時にマシンコードを生成する技術は、みんな共通しています.
最後に、iOSはJITコンパイルを禁止しており、書き込み権限と実行権限を同時に持つメモリを申請することはできません.では、JITのコアベースは、運転時に実行可能なマシンコードを生成することはできません.
公衆番号HinusWeeklyから
転載先:https://www.cnblogs.com/msymm/p/9395310.html
Just In Time
Just in timeコンパイルは、ランタイムコンパイルとも呼ばれ、C/C++言語とは異なり直接機械命令に翻訳され、javacはjavaのソースファイルをclassファイルに翻訳し、classファイルはすべてJavaバイトコードである.では、JVMはこれらのclassファイルをロードした後、これらのバイトコードに対して、1つずつ取り出し、1つずつ実行する方法であり、この方法は実行を説明することである.
もう1つは,これらのJavaバイトコードを再コンパイル最適化し,マシンコードを生成し,CPUに直接実行させることである.このようにして作成されたコードはより効率的になります.通常,すべてのJavaメソッドをマシンコードにコンパイルする必要はなく,最も頻繁に呼び出され,CPUを占有する時間が最も長いメソッドを探し出してマシンコードにコンパイルするだけである.この呼び出しが最も頻繁なJavaメソッドは、私たちがよく言うホットスポットメソッド(Hotspot、この仮想マシンの名前がここから来たとは限らない)です.
このような実行時に必要に応じてコンパイルする方法がJust In Timeである.
主な技術点
実はJITの主な技術点は、大きなフレームワークから言えば、とても簡単で、書く権限と実行権限のあるメモリを申請して、それからあなたがコンパイルするJavaの方法を、マシンコードに翻訳して、このメモリに書きます.元のJavaメソッドを呼び出す必要がある場合は、このメモリを呼び出すに移ります.
例を見てみましょう
#include
int inc(int a) { return a + 1; } int main() { printf("%d
", inc(3)); return 0; }
上の例は簡単です.3に1を加えて印刷します.次のコマンドで、マシンコードを確認します.
# gcc -o inc inc.c
# objdump -d inc
そして、この出力の山では、incメソッドが最終的にこのようなマシンコードに翻訳されることがわかります.
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d fc mov %edi,-0x4(%rbp)
400534: 8b 45 fc mov -0x4(%rbp),%eax
400537: 83 c0 01 add $0x1,%eax
40053a: 5d pop %rbp
40053b: c3 retq
説明します(読者は一定のx 86アセンブリ言語の知識が必要です).
最初の文では、前のスタックフレームのベースアドレスを保存し、現在のスタックポインタをスタックベースレジスタに割り当てます.これは、関数に入る通常の動作です.私たちはそれを気にしない.
第三句、ediをスタックに保存します.x 64プロセッサでは、最初の6つのパラメータはレジスタを使用してパラメータを転送します.最初のパラメータはrdi、2番目のパラメータはrsiなどを使用します.だからediに保存されているのは実は最初のパラメータ、つまり整数3で、なぜrdiの32ビット低い、つまりediを使うのでしょうか.私たちの入参aはint型ですからね.long型に変えて効果を見ることができます.
4番目の文は、前のステップでスタックに保存した整数をeaxに保存します.
5文目以降、eaxに1を加えて、スタックを退き、戻ります.x 64の規定(ABI)に従って、戻り値はeaxを介して伝達される.
私たちは見て、実は第3文、第4文はまったく存在する必要がないようで、gccのデフォルトの情況の下で、生成したマシンコードは少し馬鹿で、それはいつも入参をスタックの上に置くことができて、しかし実は、私たちは直接rdiの中からraxの中にパラメータを入れることができます.気に入らない.では、自分で直して、もっと簡素にすることができます.どうしますか.答えは実行時にincの論理を修正することです.
#include
#include #include typedef int (* inc_func)(int a); int main() { char code[] = { 0x55, // push rbp 0x48, 0x89, 0xe5, // mov rsp, rbp 0x89, 0xf8, // mov edi, eax 0x83, 0xc0, 0x01, // add $1, eax 0x5d, // pop rbp 0xc3 // ret }; void * temp = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); memcpy(temp, code, sizeof(code)); inc_func p_inc = (inc_func)temp; printf("%d
", p_inc(7)); return 0; }
この例では、mmapを使用して書き込み権限と実行権限のあるメモリを申請し、手書きのマシンコードをコピーし、関数ポインタを使用してメモリを指し、呼び出します.このようにして、この手書きのマシンコードを実行することができます.
実行してみます.
# gcc -o inc inc.c
# ./inc
8
この過程をもう一度思い出してください.私たちは手書きマシンコードで元のinc関数を置き換えた.新しい例では,プログラムで定義したデータを用いてinc関数を再構築した.このように実行中のプロセスで新しい関数を作成する方法がJITのコア操作である.
インタプリタ,C 1とC 2
Hotspotでは,インタプリタはバイトコードごとに小さなマシンコードを生成し,Javaメソッドを実行する過程で命令を1つずつ取り,その命令に対応するマシンコードを実行する.256個の命令は、テーブルを構成し、このテーブルでは、各命令がマシンコードに対応し、ある命令を実行すると、このテーブルからこのマシンコードを調べ、jmp命令によってこのマシンコードを実行すればよい.
この方式をテンプレート解釈器と呼ぶ.
テンプレート解釈器が生成するコードには、上記の最初の例のように冗長性がたくさんあります.より簡素なマシンコードを生成するために,グローバル値符号化,デッドコード除去,スカラー展開,共通サブ表現除去,定数伝搬などのコンパイラ最適化手段を導入することができる.このようにして生成されたマシンコードはより最適化されます.
しかし,マシンコードを生成する品質が高いほど,所要時間が長くなる.JITスレッドもJavaアプリケーションスレッドのリソースを絞り込む.だからC 1はトレードオフで、コンパイル時間はあまり長くなく、生成されたマシンコードの命令も最適化されていませんが、解釈器よりも効率が高いに違いありません.
Javaメソッドが十分に頻繁に呼び出されると、より良質なマシンコードを生成するのに苦労する価値があります.このとき、C 2コンパイルがトリガーされ、c 2はより遅く動作しますが、より効率的なコードを生成できるコンパイラです.
このことから,実際にJavaの動作は,ほとんどが実行時に生成されたマシンコードに依存していることが分かる.だから、冒頭の質問に「JavaはC++で実行されていますか?」という質問には、みんな自分の答えがあるはずです.この質問は簡単には答えられませんが、正しい答えはJavaの実行依存テンプレート解釈器とJITコンパイラです.
もっと最適化して
私たちのこの授業で挙げた例では、inc関数に入ってからスタックを全く使用していない以上、実際にはスタックフレームを開く必要はありません.だからpush rbp,pop rbpの論理をすべて取り除くことができます.
次のように最適化されます.
char code[] = {
0x89, 0xf8, // mov edi, eax
0x83, 0xc0, 0x01, // add $1, eax
0xc3 // ret
};
命令がさらに簡素化されていることがわかります.再コンパイルして実行するか、8を印刷することに成功しました.
この問題によると、なぜleaは計算に使われるのですか.
さらに最適化されたコードを書くこともできます.
char code[] = {
0x8d, 0x47, 0x01, // lea 0x1(rdi), rax
0xc3 // ret
};
gccの最適化コンパイルを開くと、例えば、この方法に対しても、このようなコードを得ることができます.
int inc(int a) {
return a + 1;
}
-O 2最適化の使用:
# gcc -o inc inc.c -O2
# objdump -d inc
incのマシンコードがこうなりました
00000000004005f0 :
4005f0: 8d 47 01 lea 0x1(%rdi),%eax
4005f3: c3 retq
これは私たちが手書きで最適化したマシンコードと全く同じです.
実際,C 1とC 2が行うべきことはgccの最適化コンパイルと同様であり,特定の方法を用いてより効率的なマシンコードを生成することである.しかし、原理的には、運転時にマシンコードを生成する技術は、みんな共通しています.
最後に、iOSはJITコンパイルを禁止しており、書き込み権限と実行権限を同時に持つメモリを申請することはできません.では、JITのコアベースは、運転時に実行可能なマシンコードを生成することはできません.
公衆番号HinusWeeklyから
転載先:https://www.cnblogs.com/msymm/p/9395310.html