1

つまり、インタープリターは、通常は整数として格納された一連のバイトによって多かれ少なかれ構成されているように見える命令のリストで作業します。オペコードは、すべての操作が配置されている大きな switch ステートメントで使用するために、ビット単位の操作を実行することによって、これらの整数から取得されます。

私の具体的な質問は次のとおりです。オブジェクトの値はどのように保存/取得されますか?

たとえば、(非現実的に)次のように仮定します。

  1. 私たちの命令は、符号なしの 32 ビット整数です。
  2. オペコード用に整数の最初の 4 ビットを予約しました。

オペコードと同じ整数でデータを格納したい場合、24 ビット整数に制限されます。次の命令に格納したい場合は、32 ビット値に制限されます。

文字列のような値には、これよりも多くのストレージが必要です。ほとんどの通訳者は、効率的な方法でこれをどのように回避していますか?

4

2 に答える 2

2

まず、あなたが主に (排他的ではないにしても) バイトコード インタープリターまたは同様のものに関心があると仮定することから始めます (あなたの質問はそれを前提としているようです)。ソース コードから (生またはトークン化された形式で) 直接動作するインタープリターは、かなり異なります。

典型的なバイトコード インタープリターの場合、基本的に理想化されたマシンを設計します。スタックベース (または少なくともスタック指向) の設計は、この目的ではかなり一般的であるため、それを想定してみましょう。

そこで、まずオペコードに 4 ビットを選択することを考えてみましょう。ここでの多くは、サポートしたいデータ形式の数と、それを op コードの 4 ビットに含めるかどうかによって異なります。議論のために、仮想マシンが適切にサポートする基本的なデータ型が 8 ビットおよび 64 ビットの整数 (アドレス指定にも使用可能)、および 32 ビットおよび 64 ビットの浮動小数点であると仮定しましょう。

整数については、少なくともサポートする必要があるものはほとんどありません。読み込み、保存します。浮動小数点は同じ算術演算をサポートしますが、論理/ビット演算を削除します。また、いくつかの分岐/ジャンプ操作 (無条件ジャンプ、ゼロの場合のジャンプ、ゼロでない場合のジャンプなど) も必要になります。スタック マシンの場合、少なくともいくつかのスタック指向の命令 (push、pop、dupe、おそらく回転など)

これにより、データ型の 2 ビット フィールドと、オペコード フィールドの少なくとも 5 (おそらく 6) ビットが得られます。特別な命令である条件付きジャンプの代わりに、1 つのジャンプ命令と、任意の命令に適用できる条件付き実行を指定するための数ビットが必要になる場合があります。また、少なくともいくつかのアドレッシング モードを指定する必要があります。

  1. オプション: 小さい即値 (命令自体の N ビットのデータ)
  2. ラージ・イミディエート (命令に続く 64 ビット・ワードのデータ)
  3. 暗黙的 (スタック上のオペランド)
  4. 絶対(命令に続く64ビットで指定されるアドレス)
  5. 相対 (命令または命令の後に指定されたオフセット)

ここでは、すべてを最小限に抑えるために最善を尽くしましたが、効率を向上させるためにもっと多くのことを望むかもしれません。

とにかく、このようなモデルでは、オブジェクトの値はメモリ内のいくつかの場所にすぎません。同様に、文字列はメモリ内の 8 ビット整数のシーケンスです。オブジェクト/文字列のほぼすべての操作は、スタックを介して行われます。たとえば、クラス A と B が次のように定義されているとします。

class A { 
    int x;
    int y;
};

class B { 
    int a;
    int b;
};

...そして次のようなコード:

A a {1, 2};
B b {3, 4};

a.x += b.a;

初期化は、a と b に割り当てられたメモリ位置にロードされた実行可能ファイル内の値を意味します。追加すると、次のようなコードが生成されます。

push immediate a.x   // put &a.x on top of stack
dupe                 // copy address to next lower stack position
load                 // load value from a.x
push immediate b.a   // put &b.a on top of stack
load                 // load value from b.a
add                  // add two values
store                // store back to a.x using address placed on stack with `dupe`

適切な命令ごとに 1 バイトを想定すると、シーケンス全体で約 23 バイトになり、そのうちの 16 バイトがアドレスになります。64 ビットの代わりに 32 ビットのアドレッシングを使用すると、それを 8 バイト削減できます (つまり、合計 15 バイト)。

心に留めておくべき最も明白なことは、典型的なバイトコード インタープリター (または同様のもの) によって実装された仮想マシンは、ハードウェアに実装された "実際の" マシンとそれほど変わらないということです。実装しようとしているモデルにとって重要な命令をいくつか追加することもできます (たとえば、JVM にはセキュリティ モデルを直接サポートするための命令が含まれています)。それらを含めます(たとえば、xor本当にしたいのであれば、いくつかのようなものを除外できると思います)。また、サポートする仮想マシンの種類を決定する必要もあります。上で説明したのはスタック指向ですが、必要に応じてレジスタ指向のマシンを実行することもできます。

いずれにせよ、オブジェクト アクセス、文字列ストレージなどのほとんどは、それらがメモリ内の場所であることに帰着します。マシンは、これらの場所からスタック/レジスタにデータを取得し、必要に応じて操作して、目的のオブジェクトの場所に保存します。

于 2013-11-14T06:15:01.603 に答える
1

私がよく知っているバイトコード インタープリターは、定数テーブルを使用してこれを行います。コンパイラがソースのチャンクのバイトコードを生成するとき、そのバイトコードに付随する小さな定数テーブルも生成します。(たとえば、バイトコードがある種の「関数」オブジェクトに詰め込まれると、定数テーブルもそこに入ります。)

コンパイラは、文字列や数値などのリテラルに遭遇するたびに、インタープリターが処理できる値の実際のランタイム オブジェクトを作成します。それを定数テーブルに追加し、値が追加されたインデックスを取得します。次にLOAD_CONSTANT、値が定数テーブルのインデックスである引数を持つ命令のようなものを発行します。

次に例を示します。

static void string(Compiler* compiler, int allowAssignment)
{
  // Define a constant for the literal.
  int constant = addConstant(compiler, wrenNewString(compiler->parser->vm,
      compiler->parser->currentString, compiler->parser->currentStringLength));

  // Compile the code to load the constant.
  emit(compiler, CODE_CONSTANT);
  emit(compiler, constant);
}

実行時に命令を実装するにLOAD_CONSTANTは、引数をデコードし、オブジェクトを定数テーブルから取り出します。

次に例を示します。

CASE_CODE(CONSTANT):
  PUSH(frame->fn->constants[READ_ARG()]);
  DISPATCH();

trueやのような小さな数値や頻繁に使用される値のようなものについてはnull、専用の命令を割り当てることができますが、それは単なる最適化です。

于 2013-12-03T21:55:53.123 に答える