web-dev-qa-db-ja.com

「レジスタ」はVMにどのように実装されますか?

プロセスVM(Oracle JVM、CPython、.NET CLRなど)は通常、スタックベースまたはレジスタベースです。

レジスタベースの「レジスタ」は、レジスタベースのVMは実際には基礎となる物理CPUのレジスタですか?それとも単にRAMにメモリバイトを割り当てているだけですか?これは通常どのように実装されますか?

スタックベースのVMについてはどうですか?特定の物理CPUレジスタを直接利用していますか、それともVMメモリスロットにRAM内のメモリバッファを割り当てているだけですか?)

これは、レジスタベースのVMがスタックベースのVMよりも高速であると思われることとどのように関連していますか?

1
Aviv Cohn

JVMと.NETはどちらも "JIT" を採用しています。

これらのJITはそれぞれのバイトコード(両方ともスタックベース)を取得し、それらを使用しているプロセッサのマシンコードに変換します。したがって、CおよびC++で使用できる同じハードウェアはJITの自由に使用でき、ほとんどの場合かなりよく使用されます。これらのいくつかは、コードを生成するためのLLVMの能力を欠いていますが、x86、x64(より多くのレジスター)などに関係なく、完全なCPUレジスターセットにアクセスして使用できます。 JITを実行すると、スタックが分析され、一部のスタックの場所にはCPUレジスタのみが与えられ、その他の場所には(スレッドスタック上の)メモリの場所が与えられます。

このマッピングは、これらのVMのスタックモデルを弱めることで促進されます。これにより、変換されたマシンコードからスタックを実際に削除できます。これが機能する方法は、それらのスタックが操作の引数の提供に制限されており、動的なプッシュとポップのための実際のスタックのように使用できないことです。たとえば、プッシュアイテムをポップする後のループを作成しても、スタックにプッシュアイテムをループすることはできません。プッシュとポップは(ローカルで)バランスを取る必要があります。 VMスタックの場所とCPUレジスタの物理リソースと(マシンコード)の間に1:1のマッピングが存在する可能性があるため、これにより、実行時の翻訳されたマシンコードでVMのスタックを完全に排除できます。スレッドスタックの場所—それ以外の場合、VMスタックの概念を明示する必要はありません。言い換えると、これらのVMのスタックは、実際の「スタック」のような完全な従来のデータ構造ではありません。 "高水準言語プログラミングで見つかるかもしれないデータ構造。これは、任意のライフタイムの間、任意の量のデータを保持できます。

レジスターVMベースのJITは、スタックベースのJITと同様に、レジスター割り当てをCPUレジスターに直接アクセスするマシンコードに変換します。同様に、一部のVMレジスターは直接、CPUレジスターにのみマップされますが、その他は(スレッドスタック上の)メモリロケーションにマップされます。

JITではなく解釈するVMの実装がいくつかあります。彼らにとって、手作業で最適化されたプライマリインタープリタループは、おそらく彼らができる最善の方法です。手動で最適化されたアセンブリを使用すると、ジャンプテーブルや、大規模なインタープリターのswitchステートメントにまたがるレジスタの使用など、いくつかの改善を行うことができます。

大まかに言えば、メモリとは異なり、CPUレジスタはアドレス指定できないため、メモリ(または配列)のようにインデックスを付けることができないため、多数のCPUレジスタをスタックとして使用することは困難です。命令ではnamedのみを指定できます。上位のいくつかのスタックアイテムをCPUレジスタにキャッシュできますが、オーバーヘッドを削減しながら他のスタックアイテムを追加し、VMスタックに使用されるより多くのCPUレジスタのリターンを減少させます。(ハードウェアのカバー、CPUの高度に並列化されたハードウェアインタープリターループは、インデックスを使用して、レジスターを配列(レジスタファイルとも呼ばれる)としてアドレス指定します数ですが、このハードウェア機能は、マシンコード命令が使用するために公開されていません。)

JITテクノロジを前提として、レジスタベースのVMバイトコードがスタックベースのバイトコードよりも高速である理由はありません。両方の目的は、バイトコードを削除してネイティブマシンコードで置き換えることです。そのプロセッサ。

レジスタベースのバイトコードを実行するインタープリターは、場合によっては少数の大きな命令を実行します。これは、基本的な命令のフェッチ、デコード、および実行ループがソフトウェアの解釈における主要なボトルネックの1つであるため、インタープリターの実行を高速化することと同じです。単純なコードシーケンスの場合、レジスタベースのマシンは通常、より大きな命令を使用します。シーケンスPush a; Push b; Add; Pop cの代わりに、より簡単なAdd c,a,b命令を使用できます。ただし、スタックには複雑な式の計算やパラメーターの受け渡し(関数の呼び出しが多いために大きな問題)にも利点があるため、このような利点はワークロード間で一貫していません。

(洗練されたハードウェアはPush a; Push b; Add; Pop cのようなシーケンスを認識し、それを単一の融合命令として実行できるため、ハードウェアレベルでも明らかな欠点が解消される可能性があります。)

9
Erik Eidt

Erik Eidtが言ったことのすべてに加えて、JIT(ジャストインタイムコンパイラ)は任意に賢くすることができます。多くの場合、コードを100回実行するインタープリターと組み合わせてランタイム情報を収集し、その後で初めてコンパイラーが呼び出されます。

インタプリタは、たとえば、(i = 0; i <n; ++ i){...}のループが実際にはn <= 0でのみ実行されることを理解する場合があります。したがって、通常のコンパイラは変数をレジスターへのループ内で使用される場合、JITはまったく使用されない可能性があります。インタプリタパスからの情報に基づいて、最も頻繁に使用される変数を決定し、それらをプロセッサレジスタに入れることができます。

また、これらがスタックの最初の変数である必要がある理由はなく、任意の変数である可能性があります。 CPUに、スタックの一端に近い変数により速くアクセスできる命令セットがある場合、JITはスタックを再配置できます。コンパイラのように、同時に使用されない2つのスタック変数に同じレジスタまたはメモリアドレスを割り当てることができます。

理論的には、複数の浮動小数点変数をベクトルレジスタに割り当てることができます(ただし、これはかなり賢いJITです)。

0
gnasher729