Bytecode VM in a nutshell


高度なプログラミング言語解釈器の多くはバイトコード(Bytecode)方式で実現され、まずソースファイルを構造の簡単な仮想マシン命令、すなわちBytecodeにコンパイルし、それから解釈器仮想マシン(VM)を使用して実行される.
次に、コンピュータ仮想マシンの構成をシミュレートします.コンピュータ仮想マシンの命令フォーマット:
struct Code
{
 Byte code[4];
 Code() {}
 Code(OPCODE op, Byte va0, Byte va1, Byte va2)
 {
   code[0] = op;
   code[1] = va0;
   code[2] = va1;
   code[3] = va2;
 }
};

コンピュータ仮想マシンのコマンド:
enum OPCODE
{
 OP_LOAD = 0, //LOAD reg,num,0 : reg 

Big SwitchバージョンのVMロジックは以下の通りです.
 switch (itr->code[0])
 {
 case OP_LOAD:
 {
   int pos = itr->code[1];
   int val = itr->code[2];
   stack[pos] = val;
   itr++;
   break;
 }
 case OP_ADD:
 {
   int dst = itr->code[1];
   int src0 = itr->code[2];
   int src1 = itr->code[3];
   stack[dst] = stack[src0] + stack[src1];
   itr++;
   break;
 }
 case OP_SUB:
 {
   int dst = itr->code[1];
   int src0 = itr->code[2];
   int src1 = itr->code[3];
   stack[dst] = stack[src0] - stack[src1];
   itr++;
   break;
 }
 case OP_MUL:
 {
   int dst = itr->code[1];
   int src0 = itr->code[2];
   int src1 = itr->code[3];
   stack[dst] = stack[src0] * stack[src1];
   itr++;
   break;
 }
 case OP_DEC:
 {
   int dst = itr->code[1];
   int src0 = itr->code[2];
   int src1 = itr->code[3];
   stack[dst] = stack[src0] / stack[src1];
   itr++;
   break;
 }
 case OP_OUT:
 {
   int dst = itr->code[1];
   printf("%.3fn", stack[dst]);
   itr++;
   break;
 }
 default:
   return;
 }

関数ポインタ(Function pointer)リストの実装をシミュレートします.
typedef void (*ExecCode)(Byte arg0, Byte arg1, Byte arg2);
HashMap dispatchMap;
….
dispatchMap.find(opcode)(op.code[1],op.code[2],op.code[3]);
….

関数呼び出しにはスタック操作に余分な時間がかかります.純粋な64ビット環境では、パラメータ数が限られており、タイプが特に指定された単純なタイプの場合、callはjmpとあまり速く差がありませんが、BigSwitchの表現は関数命令リストよりも強い場合が多いです.
Threadingという初期の言語の使い方もたくさんあります.BigSwitchの問題は,各命令の実行にjmpが何度も必要であることである.
GNU GCCコンパイラには2つの非難されている拡張子があります.それは&labelとgoto(void)です.新版のLLVMもこの拡張子をサポートしています.公式にはAddress of labelとIndirect Branchesと呼ばれています.
このように見えます.
static const void *labelAddr = &&LABEL
goto *(labelAddr);
...
LABEL:
 //do something

しかし残念ながらVisual C++とIntel C++コンパイラはこの拡張をサポートしていないので、機知のある外国人はイントラアセンブリを使ってシミュレーションし、マクロを使って条件コンパイルをすればそうすることができます.
#ifdef _WIN32
# define STORE_LABEL(index,label) __asm lea eax, label\
 __asm mov edx,_llistd\
 __asm mov [edx][index * TYPE _llistd],eax
# define GOTO_LABEL(addr) __asm jmp addr
#else
# define STORE_LABEL(index,label) _llist[index] = &&label
# define GOTO_LABEL(addr) goto *(addr)
#endif

そしてIndirect Threadingの解釈器を楽しく実現することができます.
MARK_START:
 idx = itr->code[0];
 addr = _llist[idx];
 GOTO_LABEL(addr);
MARK_LOAD:
 pos = itr->code[1];
 val = itr->code[2];
 stack[pos] = val;
 itr++;
 idx = itr->code[0];
 addr = _llist[idx];
 GOTO_LABEL(addr);
MARK_ADD:
 dst = itr->code[1];
 src0 = itr->code[2];
 src1 = itr->code[3];
 stack[dst] = stack[src0] + stack[src1];
 itr++;
 idx = itr->code[0];
 addr = _llist[idx];
 GOTO_LABEL(addr);
MARK_SUB:
 …
MARK_STOP:
 return;
MARK_INIT:
 STORE_LABEL(OP_LOAD, MARK_LOAD);
 STORE_LABEL(OP_ADD, MARK_ADD);
 STORE_LABEL(OP_SUB, MARK_SUB);
 STORE_LABEL(OP_MUL, MARK_MUL);
 STORE_LABEL(OP_DEC, MARK_DEC);
 STORE_LABEL(OP_OUT, MARK_OUT);
 STORE_LABEL(OP_STOP, MARK_STOP);
 goto MARK_START;
}

本当に素敵に見えますが、残念ながら2つの大きな問題があります.
  • Windows/VC++プラットフォームでは、アセンブリバージョンのIndirect ThreadingがBig Switchよりも遅く、かなり遅い!
  • Windows/VC++プラットフォームでは、X 64コンパイラはインラインアセンブリをサポートしていません.64ビットを放棄するか、Intel C++コンパイラを転用します.

  • 実際にGCCコンパイルを使用すると、-O 2最適化オプションを使用せずに直接コードを生成すると、結果はBigSwitchのほうが速く、最適化後、Indirect ThreadingのバージョンはBigSwitchのバージョンより約10%速くなります.
    インラインアセンブリバージョンとGCC非最適化バージョンが遅いのは、そのジャンプです.
    VC++最終コンパイル後のアセンブリ命令:
    jmp DWORD PTR _addr$[ebp]

    GCCが最適化されたアセンブリ命令を開く:
    jmp *%rax

    これに鑑みて、一部のコンパイラは、LuaJITなどのBytecode VMを実装するためにアセンブリを直接使用することを選択している.