8.1 VM engine

VM エンジンは、 VM コードを実行するための VM インタープリターです。 それはインタープリター的システムにとって不可欠なものです。

Vmgen は、 VM 命令ディスパッチ(dispatch;切り替え作業)の 2 つの方法をサポートしています。 「スレッド化コード」(高速ですが gcc 固有)と、 「スイッチ・ディスパッチ」(低速ですが、 C 言語コンパイラ間で移植可能)です。 条件付きコンパイル(‘defined(__GNUC__)’)を使用してこれらの方法を選択でき、 著者達の例ではそうしています。

どちらの方法でも、 VM エンジンは C 言語レベルの関数に含まれています。 Vmgen は関数の内容のほとんどを生成しますが、 この関数の定義と、エンジンで使用されるマクロと変数の定義と、 変数の初期化を行う必要があります。 私達の例では、 エンジン関数には name-labels.i も含まれています(see VM instruction table)。

コードの実行に加えて、 VM エンジンはオプションで、 実行された命令や、 実行された命令の引数や、 実行された命令の結果のトレースを出力することもできます。 スーパー命令(superinstructions)の場合は、 コンポーネント命令のみが実行されたかのようにトレースを出力します。 これにより、 古いスーパー命令と比較可能なトレースを維持しながら、 新しいスーパー命令を導入することができます(これは デグレ・チェック(regression tests)のために重要です)。

トレース・コードを出力するかどうかを各命令でチェックするとパフォーマンスが大幅に低下するため、 エンジンのコピーを 2 つ作成することをお勧めします。 1 つは高速実行用、 もう 1 つはトレース用です。 例については、 vmgen-ex/Makefileengine.oengine-debug.o の makeルールを参照してください。

以下のマクロと変数が name-vm.i で使用されます:

LABEL(inst_name)

これは、 ジャンプまたは switch ラベルを提供するために各 VM 命令の直前で使用されます(‘:’ は Vmgen によって提供されます)。 スイッチ・ディスパッチの場合、 これは ‘case label:’ に展開されるべきです。 スレッド化コード・ディスパッチの場合は、 これを ‘label:’ に展開するだけです。 どちらの場合も、 label は通常、 名前の競合を避けるために何らかのプレフィックスまたはサフィックスを付けた inst_name です。

LABEL2(inst_name)

これは動的スーパー命令(dynamic superinstructions)に使用されます。 現時点では、 これは「無」(nothing)に展開されるべきです。

NAME(inst_name_string)

VM 命令の名前をパラメーターとして含む文字列を含む VM 命令を入力すると呼び出されます。 通常の実行では、 これは「無」(nothing)に展開されるはずですが、 トレースの場合は通常、 名前と、 場合によってはその他の情報(この例ではいくつかの VM レジスター)が出力されます。

DEF_CA

通常は空(empty)です。 VM 命令の開始時に新しいスコープ内で呼び出されます。 すべての VM 命令中に表示される変数を定義するために使用できます。 このマクロを空ではないものとして定義する場合は、 マクロ定義は ‘;’ で終了させる必要があります。

NEXT_P0 NEXT_P1 NEXT_P2

命令ディスパッチ用の 3 つの部品。 さまざまなプロセッサで最高のパフォーマンスを得るために、 さまざまな方法で定義できます(例の engine.c または Gforth の engine/threaded.h を参照)。 ‘NEXT_P0’ は VM 命令の開始直後(ただし ‘DEF_CA’ の後)、 ‘NEXT_P1’ はユーザー指定の C 言語コードの直後、 そして ‘NEXT_P2’ は最後に呼び出されます。 実際のジャンプは ‘NEXT_P2’ によって実行する必要があります(これを実行するのが早すぎると、 VM 命令の重要な部分が実行されなくなります)。

バリエーションの最も単純なモノは、 ‘NEXT_P2’ がすべてを行い、 他のマクロは何も実行しないヤツです。 ‘IP’ や ‘SET_IP’ や ‘IP’ や ‘INC_IP’ や ‘IPTOS’ などの関連マクロも非常に簡単に定義できます。 スイッチ・ディスパッチの場合、 このコードはディスパッチ・コード(この例では ‘goto next_inst;’) へのジャンプだけで構成されます。 直接スレッド化コード(direct threaded code)の場合は、 ‘({cfa=*ip++; goto *cfa;})’ のようなもので構成されます。

コード(普通は ‘cfa=*ip++;’ というコード)を ‘NEXT_P1’ にセットすることは通常問題を引き起こしませんが、 ‘NEXT_P0’ にコードをセットすると、 通常、他のマクロも変更する必要があります(そして、 少なくとも Alpha 版の Gforth については、 コンパイラーは関連するものを自動的にスケジュール設定することが多いため、 それほど大きな価値はありません)。 さらに極端なバリエーションとしては、 コードをさらに離れた、 たとえば前の VM 命令の NEXT_P1 にセットすることです (プリフェッチ。 PowerPC で便利です)。

INC_IP(n)

IPn を足しこみます。

SET_IP(target)

IPtarget をセットします。

vm_A2B(a,b)

(A 型の) ‘a’ を (B 型の) ‘b’ に割り当てる型キャスト・マクロ。 これは主に、 スタック項目を変数に入れたりスタック項目から変数へ戻したりするために使用されます。 したがって、 スタックの基本型(この例では Cell)と、 そのスタックで使用される型プレフィックス形式(スタック項目を変数に入れる方向とスタック項目から変数へ戻す方向)すべての組み合わせに対してマクロを定義する必要があります。 型プレフィックス形式の場合は、 (C言語の型名ではない)型プレフィックスを型名(‘vm_Cell2Cell’ ではなく ‘vm_Cell2i’)として使用します。 さらに、 スタックの基本型 X に対して(スーパー命令で使われる) vm_X2X マクロを定義する必要があります。

事前定義された ‘inst-stream’ (命令ストリーム)のスタック基本型は ‘Cell’ です。 同一項目サイズのスタックが必要な場合、 その基本型を ‘Cell’ にすると、 通常、 定義する必要があるマクロの数が減ります。

ただし、 私達の例では大きく異なります: vmgen-ex はこれらのマクロでキャストを使用しますが、 vmgen-ex2 は共用体(union)フィールドの選択(または共用体フィールドへの代入)を使用します。 浮動小数点数(floats)を整数にキャストしたり、 その逆を行うとビット・パターンが変更されることに注意してください(そして、 これはあなたが望む事ではありません)。 この場合の選択肢は、 (一時的な(temporary))共用体を使用するか、 値のアドレスを取得してポインタをキャストし、 それを逆参照(dereference)することです(常に可能であるとは限らず、 しばしばコストがかさみます)。

vm_twoA2B(a1,a2,b)
vm_B2twoA(b,a1,a2)

2 つのスタック項目(a1a2)と 2 つのスタック項目を取る型の変数 b の間の型キャスト。 これは私達の小さな例では発生しませんが、 例として Gforth を参照してください(engine/forth.hvm_twoCell2d を参照してください)。

stackpointer

使用されるスタックごとに、 スタック宣言で指定されたスタック・ポインター名が使用されます。 通常のスタックの場合、 これは左辺値に解決される式(l-expression)でなければなりません。 通常、 これはスタックの基本型へのポインタとして宣言された変数です。 ‘inst-stream’ (命令ストリーム)の場合、 名前は ‘IP’ で、 プレーンな右辺値を指定できます。 通常、 これは NEXT_P* のさまざまな実装間の違いを抽象化するマクロです。

IMM_ARG(access,value)

これを "(access)" に展開するように定義します。 これは将来の拡張機能のための単なるプレースホルダーです。

stackpointerTOS

stackpointer が指し示すスタックのTOS(スタックのトップ)。 そのスタックに対してスタックのトップ・キャッシュを使用している場合は、 これを変数として定義する必要があります。 そのスタックがスタックのトップ・キャッシュを使用していない場合、 これは ‘stackpointer[0]’ に展開されるマクロである必要があります。 事前定義された ‘inst-stream’ (命令ストリーム)のスタック・ポインターは ‘IP’ と呼ばれるため、 TOS(スタックの最上位)は ‘IPTOS’ と呼ばれます。

IF_stackpointerTOS(expr)

stackpointer が指し示すスタックにスタックのトップ・キャッシュが使用されている場合に、 expr を実行するためのマクロ。 つまり、 stackpointer のスタックのトップ・キャッシュがある場合に expr を実行する必要があります。 それ以外の場合は何もしません。

SUPER_END

これは VM プロファイラー(see VM profiler)によって使用されます。 通常の操作では何も行わないので、 プロファイリングのためには vm_count_block(IP) を呼び出す必要があります。

SUPER_CONTINUE

これは Vmgen への単なるヒントであり、 C 言語レベルでは何も行いません。

MAYBE_UNUSED

gcc-2.7 以降では、 これを __attribute__((unused)) として定義する必要があります。 スーパー命令(superinstructions)のコード内の未使用の変数に関する警告が抑制されます。 これを定義する必要があるのは、 あなたがスーパー命令を使用している場合のみです。

VM_DEBUG

これが定義されている場合、 トレース・コードがコンパイルされます(解釈(interpretation)は遅くなりますが、 デバッグは向上します)。 私達の例では、 エンジンの 2 つのバージョンをコンパイルします: 1 つはトレースできない高速実行バージョンで、 もう 1 つはトレースとプロファイリングが可能です。

vm_debug

VM_DEBUG’ が定義されている場合にのみ必要です。 この変数に true が含まれている場合、 VM 命令はトレース出力を生成します。 いつでもオンまたはオフにできます。

vm_out

VM_DEBUG’ が定義されている場合にのみ必要です。 トレース出力を印刷するファイルを指定します(‘FILE *’ とタイプします)。

printarg_type(value)

VM_DEBUG’ が定義されている場合にのみ必要です。 type に適した方法で value を出力するためのマクロまたは関数です。 これは、 トレース中にスタック項目の値を出力するために使用されます。 Type は通常、type-prefix 定義で指定された型プレフィックスです(例: ‘printarg_i’)。 スーパー命令(superinstructions)では、 これは現在のスタックの基本タイプです。