26

非 JIT VM としては十分なパフォーマンスを発揮する仮想マシンを C で作成しましたが、何か新しいことを学び、パフォーマンスを向上させたいと考えています。私の現在の実装では、スイッチを使用して VM バイトコードから命令に変換し、ジャンプ テーブルにコンパイルします。私が言ったように、それが何であるかについてはまともなパフォーマンスですが、JITコンパイラでしか克服できない障壁にぶつかりました.

少し前に自己変更コードについて同様の質問をしましたが、適切な質問をしていないことに気付きました。

したがって、私の目標は、この C 仮想マシン用の JIT コンパイラを作成することであり、x86 アセンブリで実行したいと考えています。(アセンブラとして NASM を使用しています) これを行う方法がよくわかりません。私はアセンブリに慣れており、自己変更コードの例をいくつか調べましたが、コード生成の方法はまだわかりません。

これまでの主なブロックは、引数を使用て命令を実行可能なメモリにコピーすることです。NASM で特定の行にラベルを付け、静的引数を使用してそのアドレスから行全体をコピーできることは承知していますが、これはあまり動的ではなく、JIT コンパイラでは機能しません。バイトコードから命令を解釈し、それを実行可能メモリにコピーし、最初の引数を解釈してメモリにコピーし、次に 2 番目の引数を解釈してメモリにコピーできる必要があります。

GNU lightning や LLVM など、この作業を容易にするいくつかのライブラリについて知らされています。ただし、外部リソースを使用する前に、これがどのように機能するかを理解するために、最初にこれを手動で記述したいと思います。

このタスクを開始するために、このコミュニティが提供できるリソースや例はありますか? 「add」や「mov」などの 2 つまたは 3 つの命令を使用して実行可能コードを生成し、引数をメモリ内で動的に生成する単純な例は、驚くべきことです。

4

2 に答える 2

19

アセンブリで JIT を書くことはまったくお勧めしません。インタプリタの最も頻繁に実行されるビットをアセンブリで記述することには、適切な議論があります。これがどのように見えるかの例については、LuaJIT の作成者であるMike Pall からのこのコメントを参照してください。

JIT に関しては、さまざまな複雑さを持つさまざまなレベルがあります。

  1. インタプリタのコードをコピーするだけで、基本ブロック (分岐しない命令のシーケンス) をコンパイルします。たとえば、いくつかの (レジスタベースの) バイトコード命令の実装は次のようになります。

    ; ebp points to virtual register 0 on the stack
    instr_ADD:
        <decode instruction>
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        <dispatch next instruction>
    instr_SUB:
        ... ; similar
    

    したがって、命令シーケンスが与えられた場合ADD R3, R1, R2SUB R3, R3, R4単純な JIT は、インタープリター実装の関連部分を新しいマシン コード チャンクにコピーできます。

        mov ecx, 1
        mov edx, 2
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        mov ecx, 3
        mov edx, 4
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        sub eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
    

    これは単に関連するコードをコピーするだけなので、それに応じて使用されるレジスタを初期化する必要があります。より良い解決策は、これを機械命令に直接変換するmov eax, [ebp + 4]ことですが、要求された命令を手動でエンコードする必要があります。

    この手法により、解釈のオーバーヘッドが取り除かれますが、それ以外の場合、効率はあまり向上しません。コードが 1 回か 2 回しか実行されない場合は、最初にマシン コードに変換する価値はありません (少なくとも I キャッシュの一部をフラッシュする必要があります)。

  2. 一部の JIT はインタープリターの代わりに上記の手法を使用しますが、頻繁に実行されるコードに対してより複雑な最適化メカニズムを採用しています。これには、実行されたバイトコードを追加の最適化が実行される中間表現 (IR) に変換することが含まれます。

    ソース言語と JIT の種類によっては、これが非常に複雑になる場合があります (これが、多くの JIT がこのタスクを LLVM に委譲する理由です)。メソッドベースの JIT は、制御フロー グラフの結合を処理する必要があるため、SSA フォームを使用し、それに対してさまざまな分析を実行します (例: Hotspot)。

    トレース JIT (LuaJIT 2 など) は直線コードのみをコンパイルするため、多くの実装が容易になりますが、トレースの選択方法と複数のトレースを効率的にリンクする方法には十分注意する必要があります。Gal と Franz は、この論文 (PDF)で 1 つの方法について説明しています。別の方法については、LuaJIT ソース コードを参照してください。どちらの JIT も C (またはおそらく C++) で記述されています。

于 2011-02-18T00:11:21.093 に答える
8

プロジェクトhttp://code.google.com/p/asmjit/をご覧になることをお勧めします。提供されるフレームワークを使用することで、多くのエネルギーを節約できます。すべてを手書きで書きたい場合は、ソースを読んで自分で書き直すだけで、それほど難しいことではないと思います。

于 2011-07-28T01:30:25.490 に答える