22

ショックを受けないでください。これは多くのテキストですが、詳細な情報を提供しないと、これが何であるかを実際に示すことはできません (そして、私の質問に実際には対応していない多くの回答が得られる可能性があります)。そして、これは間違いなく割り当てではありません(誰かが彼のコメントでばかげて主張したように)。

前提条件

この質問は、少なくともいくつかの前提条件が設定されていない限り、おそらくまったく答えられないため、前提条件は次のとおりです。

  • 仮想マシン コードは解釈されます。JIT コンパイラーが存在することは禁止されていませんが、設計はインタープリターをターゲットにする必要があります。
  • VM は、スタック ベースではなく、レジスタ ベースである必要があります。
  • 答えは、レジスタの固定セットがあることも、それらの数に制限がないことも想定していない可能性があります。

さらに、「より良い」のより良い定義が必要です。考慮しなければならないプロパティがいくつかあります。

  1. ディスク上の VM コードのストレージ領域。もちろん、ここですべての最適化を破棄してコードを圧縮することもできますが、これは (2) に悪影響を及ぼします。
  2. デコード速度。コードを直接実行できるものに変換するのに時間がかかりすぎる場合、コードを保存する最善の方法は役に立ちません。
  3. メモリ内のストレージ スペース。このコードは、さらにデコードするかどうかに関係なく、直接実行できる必要がありますが、さらにデコードが必要な場合、このエンコードは実行中および命令が実行されるたびに行われます (コードのロード時に 1 回だけ行われるデコードは項目 2 にカウントされます)。
  4. コードの実行速度 (一般的なインタープリター手法を考慮)。
  5. VM の複雑さと、そのためのインタープリターを作成するのがいかに難しいか。
  6. VM 自体が必要とするリソースの量。(VM が実行するコードのサイズが 2 KB で、瞬きするよりも速く実行される場合、それは適切な設計ではありませんが、これを行うには 150 MB が必要であり、その起動時間はコードの実行時間よりもはるかに長くなります。実行されます)

ここで、多かれ少なかれオペコードが実際に意味することの例を示します。操作ごとに 1 つのオペコードが必要なため、オペコードの数が実際に設定されているように見える場合があります。ただし、それほど簡単ではありません。

同じ操作に対する複数のオペコード

のような操作ができます。

ADD R1, R2, R3

R1 と R2 の値を加算し、結果を R3 に書き込みます。ここで、次の特殊なケースを検討してください。

ADD R1, R2, R2
ADD R1, 1, R1

これらは、多くのアプリケーションで見られる一般的な操作です。既存のオペコードでそれらを表現できます (最後のオペコードがレジスタではなく int 値を持っているために別のオペコードが必要な場合を除く)。ただし、これらに対して特別なオペコードを作成することもできます。

ADD2 R1, R2
INC R1

前と同じ。メリットはどこ?ADD2 は 3 つではなく 2 つの引数のみを必要とし、INC は 1 つしか必要としません。したがって、これはディスク上および/またはメモリ内でよりコンパクトにエンコードできます。いずれかの形式を別の形式に変換することも簡単であるため、デコード ステップでこれらのステートメントを表現するために両方の方法の間で変換できます。ただし、どちらの形式が実行速度にどの程度影響するかはわかりません。

2 つのオペコードを 1 つに結合する

ここで、ADD_RRR (レジスターの R) とデータをレジスターにロードするための LOAD があるとします。

LOAD value, R2
ADD_RRR R1, R2, R3

これら 2 つのオペコードを使用して、コード全体で常にこのような構成を使用できます...または、それらを ADD_RMR (メモリの M) という名前の単一の新しいオペコードに結合できます。

ADD_RMR R1, value, R3

データ型とオペコード

ネイティブ型として 16 ビット整数と 32 ビット整数があるとします。レジスタは 32 ビットなので、どちらのデータ型にも適合します。2 つのレジスタを追加すると、データ型をパラメーターにすることができます。

ADD int16, R1, R2, R3
ADD int32, R1, R2, R3

たとえば、符号付き整数と符号なし整数についても同じことが言えます。そうすれば、ADD は 1 バイトの短いオペコードになり、別のバイト (または単に 4 ビット) が VM にレジスタの解釈方法を伝えます (レジスタは 16 ビットまたは 32 ビットの値を保持しますか)。または、型エンコーディングを破棄して、代わりに 2 つのオペコードを使用できます。

ADD16 R1, R2, R3
ADD32 R1, R2, R3

どちらもまったく同じだと言う人もいるかもしれません.16ビットのオペコードとして最初の方法を解釈するだけでうまくいきます. はい、しかし非常にナイーブなインタープリターは、かなり異なって見えるかもしれません。たとえば、オペコードごとに 1 つの関数があり、switch ステートメントを使用してディスパッチする場合 (最適な方法ではなく、関数呼び出しのオーバーヘッド、switch ステートメントも最適ではないことはわかっています)、2 つのオペコードは次のようになります。

case ADD16: add16(p1, p2, p3); break; // pX pointer to register
case ADD32: add32(p1, p2, p3); break;

各機能は、特定の種類の追加を中心にしています。ただし、2番目のものは次のようになります。

case ADD: add(type, p1, p2, p3); break;

// ...
// and the function

void add (enum Type type, Register p1, Register p2, Register p3)
{
    switch (type) {
       case INT16: //...
       case INT32: // ...
    }
}

サブスイッチをメイン スイッチに追加するか、サブ ディスパッチ テーブルをメイン ディスパッチ テーブルに追加します。もちろん、型が明示的であるかどうかに関係なく、インタープリターはどちらの方法でも実行できますが、オペコードの設計に応じて、どちらの方法も開発者にとってよりネイティブに感じられます。

メタ オペコード

より良い名前がないので、そのように呼びます。これらのオペコードは、それ自体ではまったく意味がなく、後続のオペコードの意味を変更するだけです。有名な WIDE 演算子のように:

ADD R1, R2, R3
WIDE
ADD R1, R2, R3

たとえば、2 番目のケースでは、レジスタは 16 ビット (したがって、より多くのアドレスを指定できます) で、最初のケースでは 8 ビットしかありません。あるいは、そのようなメタ オペコードを持たず、ADD および ADD_WIDE オペコードを持つことはできません。WIDE のようなメタ オペコードは、SUB_WIDE、MUL_WIDE などを持つことを避けます。これは、他のすべての通常のオペコードに常に WIDE を付加できるためです (常に 1 つのオペコードのみ)。不利な点は、オペコードだけでは無意味になることです。それがメタ オペコードであったかどうか、その前にオペコードを常にチェックする必要があります。さらに、VM はスレッドごとに余分な状態を保存し (たとえば、現在ワイド モードであるかどうかに関係なく)、次の命令の後に状態を再度削除する必要があります。CPU にもそのようなオペコードがあります (x86 LOCK オペコードなど)。

適切なトレードオフを見つける方法???

もちろん、オペコードが多いほど、スイッチ/ディスパッチテーブルが大きくなり、これらのコードをディスクまたはメモリで表現するために必要なビットが多くなります (ただし、データが保存されていないディスクにより効率的に格納できる場合があります)。 VM で直接実行できる必要があります)。また、VM はより複雑になり、より多くのコード行が必要になります。一方で、オペコードがより強力になります。複雑な式であっても、すべての式が 1 つのオペコードになるポイントに近づいています。

小さなオペコードを選択すると、VM のコーディングが容易になり、非常にコンパクトなオペコードにつながると思います。一方、単純なタスクを実行するには非常に多くのオペコードが必要になる可能性があり、あまり使用されない式はすべて、オペコードを使用できないため、ある種の(ネイティブ)関数呼び出しになります。

私はインターネット上のあらゆる種類の VM について多くのことを読みましたが、どちらの方向にも適切で公正なトレードオフを実際に行っている情報源はありませんでした。VM の設計は CPU の設計に似ています。オペコードがほとんどなく高速な CPU もありますが、これらの多くも必要です。また、多くのオペコードを持つ CPU があり、非常に遅いものもありますが、同じコードを表現するために必要なオペコードははるかに少なくなります。「オペコードが多いほど良い」CPU が消費者市場を完全に獲得し、「オペコードが少ないほど良い」CPU は、サーバー市場またはスーパー コンピューター ビジネスの一部でのみ生き残ることができるようです。VMはどうですか?

4

4 に答える 4

8

正直なところ、これは主にVMの目的の問題だと思います。これは、プロセッサの設計が主にプロセッサの主な使用目的によって決定される方法と同様です。

つまり、VMの一般的なユースケースのシナリオを決定できることが望ましいので、必要になる可能性のある機能を確立し、非常に一般的に必要になる可能性の低い機能も確立できます。

もちろん、他のプログラミング言語の内部/バックエンド実装として使用できる、抽象的で非常に汎用的な仮想マシンを想定していることは理解していますか?

しかし、私は、何かの「一般的な理想」の実装のようなものは実際には存在しないことを認識し、強調することが重要だと感じています。つまり、一般的で抽象的なものを維持すると、妥協が必要な状況に必然的に直面します。

理想的には、これらの妥協はコードの実際の使用シナリオに基づいているため、これらの妥協は実際には、手足に出ることなく行うことができる十分な情報に基づいた仮定と単純化に基づいています。

言い換えれば、私はあなたのVMの目標は何だと思いますか?それは主にあなたのビジョンでどのように使用されますか?あなたが達成したい目標は何ですか?

これは、要件を考え出し、単純化するのに役立ちます。これにより、合理的な仮定に基づいて命令セットを設計できます。

VMが主にプログラミング言語で数値計算に使用されることを期待している場合は、幅広いデータ型をサポートする低レベルのプリミティブを多数提供することにより、数学演算を備えたかなり強力な基盤を探すことをお勧めします。

一方、オブジェクト指向言語のバックエンドとしてサーバーを使用する場合は、対応する低レベルの命令(つまり、ハッシュ/辞書)の最適化を検討する必要があります。

一般に、最初は命令セットをできるだけシンプルで直感的に保つことをお勧めします。特別な命令を追加するのは、それらを配置することが実際に有用であり(つまり、プロファイルとオペコードのダンプ)、パフォーマンスを引き起こすことが証明された場合のみです。利得。したがって、これは主に、VMが最初に持つ「顧客」によって決定されます。

より複雑なアプローチを本当に研究したい場合は、パターンマッチングを使用してバイトコード内のオペコードの一般的な出現を見つけ、より抽象的な実装を導き出すために、実行時に命令セットを動的に最適化することを検討することもできます。カスタムのランタイム生成オペコードを使用して、バイトコードを動的に使用します。

于 2009-06-11T01:21:06.863 に答える
5

ソフトウェアのパフォーマンスに関しては、すべてのオペコードが同じ長さである方が簡単です。そのため、1 つの巨大な switch ステートメントを使用でき、前の修飾オペコードによって設定された可能性のあるさまざまなオプション ビットを調べる必要がありません。

あなたが質問しなかったと思う 2 つの問題は、プログラミング言語を VM コードに変換するコンパイラーの作成の容易さと、VM コードを実行するインタープリターの作成の容易さです。これらはどちらも、オペコードが少ないほど簡単です。(ただし、少なすぎるというわけではありません。たとえば、除算オペコードを省略した場合、優れた除算関数のコーディング方法を学ぶ機会が得られます。優れた除算関数は、単純な関数よりもはるかに困難です。)

于 2009-06-10T06:25:52.977 に答える
2

1つのオペコードに組み合わせることができるので、私は最小限の命令セットを好みます。たとえば、2つの4ビット命令フィールドで構成されるオペコードは、256エントリのジャンプテーブルを使用してディスパッチできます。ディスパッチオーバーヘッドが解釈パフォーマンスの主なボトルネックであるため、1秒おきの命令のみをディスパッチする必要があるため、パフォーマンスは約2倍に増加します。最小限で効果的な命令セットを実装する1つの方法は、アキュムレータ/ストアの設計です。

于 2010-10-23T11:25:43.967 に答える