6

私は趣味のプロジェクトとして C で仮想マシンを作成しました。この仮想マシンは、Intel シンタックス x86 アセンブリと非常によく似たコードを実行します。問題は、この仮想マシンが使用するレジスタが名前だけのレジスタであることです。私の VM コードでは、レジスタは x86 レジスタと同じように使用されますが、マシンはそれらをシステム メモリに格納します。VM コードでシステム メモリを介してレジスタを使用しても、パフォーマンスは向上しません。(ローカリティだけでも多少パフォーマンスは上がると思っていたのですが、実際には何も変わっていません。)

プログラムを解釈するとき、この仮想マシンは命令への引数をポインターとして格納します。これにより、仮想命令はメモリアドレス、定数値、仮想レジスタ、またはほぼ何でも引数として取ることができます。

ハードウェア レジスタにはアドレスがないため、VM レジスタをハードウェア レジスタに実際に格納する方法が思い浮かびません。仮想レジスタ タイプで register キーワードを使用しても機能しません。これは、仮想レジスタを引数として使用するには、仮想レジスタへのポインタを取得する必要があるためです。これらの仮想レジスタをネイティブのカウンターパートのように動作させる方法はありますか?

必要に応じて、アセンブリを詳しく調べてもまったく問題ありません。この VM コードを JIT コンパイルすることでハードウェア レジスタを利用できることは承知していますが、解釈されたコードでもそれらを使用できるようにしたいと考えています。

4

6 に答える 6

9
  1. マシン レジスタにはインデックス作成のサポートがありません。実行時に指定された「インデックス」を使用してレジスタにアクセスすることはできません。命令からレジスタ インデックスをデコードしている可能性が高いため、唯一の方法は大きなスイッチ (つまりswitch (opcode) { case ADD_R0_R1: r[0] += r[1]; break; ... }) を作成することです。インタプリタのループ サイズが大きくなりすぎて、命令キャッシュのスラッシングが発生するため、これはおそらく悪い考えです。

  2. x86 について話している場合、追加の問題は、汎用レジスタの量がかなり少ないことです。それらのいくつかは簿記に使用されます (PC の格納、VM スタック状態の格納、命令のデコードなど) - VM 用に複数の空きレジスタがある可能性は低いです。

  3. レジスタのインデックス作成がサポートされていたとしても、パフォーマンスが向上する可能性はほとんどありません。一般に、インタプリタにおける最大のボトルネックは命令のデコードです。x86 は、レジスタ値 (つまり ) に基づく高速でコンパクトなメモリ アドレス指定をサポートしているmov eax, dword ptr [ebx * 4 + ecx]ため、多くのメリットはありません。ただし、生成されたアセンブリをチェックすることは価値があります。つまり、「レジスタ プール」アドレスがレジスタに格納されていることを確認します。

  4. インタープリターを高速化する最善の方法は JITting です。単純な JIT (つまり、スマート レジスタ割り当てなし - 基本的には、命令のデコードを除いて、命令ループと switch ステートメントで実行するのと同じコードを発行するだけ) でさえ、パフォーマンスを 3 倍以上向上させることができます (これらは単純な JITter の実際の結果です) Lua のようなレジスタベースの VM の上に)。インタープリターは、参照コードとして保持するのが最適です (または、JIT メモリ コストを削減するためのコールド コード - JIT 生成コストは、単純な JIT では問題になりません)。

于 2011-01-25T07:39:41.080 に答える
3

ハードウェア レジスタに直接アクセスできたとしても、メモリの代わりにレジスタを使用するという決定にコードをラップすると、はるかに遅くなります。

パフォーマンスを得るには、パフォーマンスを事前に設計する必要があります。

いくつかの例。

すべてのトラップを設定して、仮想メモリ空​​間を離れるコードをキャッチすることにより、x86 VM を準備します。コードを直接実行し、エミュレートせずに分岐して実行します。コードがデバイスなどと通信するためにメモリ/I/Oスペースから到達すると、それをトラップし、そのデバイスまたは到達していたものをエミュレートしてから、制御をプログラムに戻します。コードがプロセッサに依存している場合は非常に高速に実行され、I/O に依存している場合は遅くなりますが、各命令をエミュレートするほど遅くはありません。

静的バイナリ変換。実行する前にコードを逆アセンブルして変換します。たとえば、命令 0x34,0x2E は .c ファイルで ascii に変わります。

アル ^= 0x2E; =0の; cf=0; sf=アル

理想的には、大量のデッド コードの削除を実行します (次の命令がフラグも変更する場合は、ここでは変更しないなど)。あとはコンパイラのオプティマイザに任せます。この方法でエミュレーターよりもパフォーマンスを向上させることができます。パフォーマンスの向上は、コードをどれだけ最適化できるかによって異なります。新しいプログラムであるため、ハードウェア上で実行され、メモリとすべてが登録されるため、プロセッサにバインドされたコードは VM よりも遅くなります。場合によっては、メモリをシミュレートしたため、メモリ/io をトラップする例外を処理するプロセッサに対処する必要はありません。コードでアクセスしますが、それでもコストがかかり、とにかくシミュレートされたデバイスを呼び出すため、そこに節約はありません。

sbt に似た動的変換ですが、実行時にこれを行います。たとえば、10 進アルファと言う他のプロセッサで x86 コードをシミュレートするときにこれが行われると聞きました。コードは x86 命令からネイティブ アルファ命令にゆっくりと変更されるため、次回はx86 命令をエミュレートする代わりに、alpha 命令を直接実行します。コードを実行するたびに、プログラムはより速く実行されます。

または、エミュレーターを再設計して、実行の観点からより効率的にすることもできます。たとえば、MAME でエミュレートされたプロセッサを見てください。コードの可読性と保守性は、パフォーマンスのために犠牲にされています。それが重要であると書かれているとき、今日のマルチコア ギガヘルツ プロセッサでは、1.5 GHz 6502 または 3 GHz z80 をエミュレートするのにそれほど苦労する必要はありません。テーブル内の次のオペコードを調べて、命令のフラグ計算の一部またはすべてをエミュレートしないことを決定するのと同じくらい簡単なことで、顕著なブーストが得られます。

要するに、プログラムの実行時に x86 ハードウェア レジスタ、Ax、BX などを使用して AX、BX などのレジスタをエミュレートすることに関心がある場合、それを行う唯一の効率的な方法は、命令を実際に実行することであり、実行して実行することではありません。デバッガーのシングル ステップのようにトラップしますが、VM 空間から出ないようにしながら、長い一連の命令を実行します。これを行うにはさまざまな方法があり、パフォーマンスの結果はさまざまであり、パフォーマンス効率の高いエミュレーターよりも高速になるわけではありません。これにより、プロセッサをプログラムに一致させることが制限されます。効率的なコードと非常に優れたコンパイラ (優れたオプティマイザ) を使用してレジスタをエミュレートすると、ハードウェアを実行中のプログラムに合わせる必要がないという点で、妥当なパフォーマンスと移植性が得られます。

于 2011-01-25T15:00:46.310 に答える
1

実行前に(事前に)複雑なレジスタベースのコードを変換します。単純な解決策は、実行用のデュアルスタックVMのようなもので、レジスタの最上位要素(TOS)をキャッシュする可能性を提供します。レジスタベースのソリューションを好む場合は、可能な限り多くの命令をバンドルする「オペコード」形式を選択してください(サムルール、MISCスタイルの設計を選択した場合、最大4つの命令を1バイトにバンドルできます)。このようにして、仮想レジスタアクセスは、各静的スーパー命令の物理レジスタ参照にローカルで解決できます(clangおよびgccはそのような最適化を実行できます)。副作用として、BTBの誤予測率が低下すると、特定のレジスタ割り当てに関係なく、パフォーマンスが大幅に向上します。

Cベースのインタープリターに最適なスレッド化手法は、直接スレッド化(アドレス拡張としてのラベル)と複製スイッチスレッド化(ANSI準拠)です。

于 2011-12-29T17:54:37.910 に答える
0

あなたが尋ねた特定の質問に答えるには:

C コンパイラに、一連のレジスタを自由に使用できるようにするように指示できます。通常、メモリの最初のページへのポインタは許可されません。これらは NULL ポインタ チェック用に予約されているため、レジスタをマークするために初期ポインタを悪用する可能性があります。いくつかのネイティブ レジスタに余裕がある場合に役立ちます。そのため、私の例では 64 ビット モードを使用して 4 つのレジスタをシミュレートしています。スイッチのオーバーヘッドが増えると、実行が速くなるどころか遅くなる可能性があります。一般的なアドバイスについては、他の回答も参照してください。

/* compile with gcc */

register long r0 asm("r12");
register long r1 asm("r13");
register long r2 asm("r14");
register long r3 asm("r15");

inline long get_argument(long* arg)
{
    unsigned long val = (unsigned long)arg;
    switch(val)
    {
        /* leave 0 for NULL pointer */
        case 1: return r0;
        case 2: return r1;
        case 3: return r2;
        case 4: return r3;
        default: return *arg;
    }
}
于 2011-01-25T15:14:01.827 に答える
0

つまり、実際のハードウェアよりも 10 の 1 ~ 3 乗遅くなる x86 インタープリターを作成しています。実際のハードウェアでは、言うのmov mem, fooは よりもはるかmov reg, fooに時間がかかりますが、プログラムでは(モジュロ キャッシュ)mem[adr] = fooと同じくらいの時間がかかります。myRegVars[regnum] = fooそれで、あなたは同じ速度差を期待していますか?

レジスタとメモリの速度差をシミュレートしたい場合は、Cachegrind と同じようなことをする必要があります。つまり、シミュレートされたクロックを保持し、メモリ参照を行うと、それに大きな数値が追加されます。

于 2011-01-25T13:46:27.013 に答える
0

あなたの VM は、効率的に解釈するには複雑すぎるようです。明らかな最適化は、レジスタのロード/ストア命令を備えた「マイクロコード」VM を使用することです。実行前に、高レベルの VM をより単純な VM に変換できます。別の便利な最適化は、gcc 計算可能ラベル拡張に依存します。このようなスレッド VM 実装の例については、Objective Caml VM インタープリターを参照してください。

于 2011-01-25T15:04:26.017 に答える