Bytecode VM in a nutshell
高度なプログラミング言語解釈器の多くはバイトコード(Bytecode)方式で実現され、まずソースファイルを構造の簡単な仮想マシン命令、すなわちBytecodeにコンパイルし、それから解釈器仮想マシン(VM)を使用して実行される.
次に、コンピュータ仮想マシンの構成をシミュレートします.コンピュータ仮想マシンの命令フォーマット:
コンピュータ仮想マシンのコマンド:
Big SwitchバージョンのVMロジックは以下の通りです.
関数ポインタ(Function pointer)リストの実装をシミュレートします.
関数呼び出しにはスタック操作に余分な時間がかかります.純粋な64ビット環境では、パラメータ数が限られており、タイプが特に指定された単純なタイプの場合、callはjmpとあまり速く差がありませんが、BigSwitchの表現は関数命令リストよりも強い場合が多いです.
Threadingという初期の言語の使い方もたくさんあります.BigSwitchの問題は,各命令の実行にjmpが何度も必要であることである.
GNU GCCコンパイラには2つの非難されている拡張子があります.それは&labelとgoto(void)です.新版のLLVMもこの拡張子をサポートしています.公式にはAddress of labelとIndirect Branchesと呼ばれています.
このように見えます.
しかし残念ながらVisual C++とIntel C++コンパイラはこの拡張をサポートしていないので、機知のある外国人はイントラアセンブリを使ってシミュレーションし、マクロを使って条件コンパイルをすればそうすることができます.
そしてIndirect Threadingの解釈器を楽しく実現することができます.
本当に素敵に見えますが、残念ながら2つの大きな問題があります. Windows/VC++プラットフォームでは、アセンブリバージョンのIndirect ThreadingがBig Switchよりも遅く、かなり遅い! Windows/VC++プラットフォームでは、X 64コンパイラはインラインアセンブリをサポートしていません.64ビットを放棄するか、Intel C++コンパイラを転用します.
実際にGCCコンパイルを使用すると、-O 2最適化オプションを使用せずに直接コードを生成すると、結果はBigSwitchのほうが速く、最適化後、Indirect ThreadingのバージョンはBigSwitchのバージョンより約10%速くなります.
インラインアセンブリバージョンとGCC非最適化バージョンが遅いのは、そのジャンプです.
VC++最終コンパイル後のアセンブリ命令:
GCCが最適化されたアセンブリ命令を開く:
これに鑑みて、一部のコンパイラは、LuaJITなどの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つの大きな問題があります.
実際にGCCコンパイルを使用すると、-O 2最適化オプションを使用せずに直接コードを生成すると、結果はBigSwitchのほうが速く、最適化後、Indirect ThreadingのバージョンはBigSwitchのバージョンより約10%速くなります.
インラインアセンブリバージョンとGCC非最適化バージョンが遅いのは、そのジャンプです.
VC++最終コンパイル後のアセンブリ命令:
jmp DWORD PTR _addr$[ebp]
GCCが最適化されたアセンブリ命令を開く:
jmp *%rax
これに鑑みて、一部のコンパイラは、LuaJITなどのBytecode VMを実装するためにアセンブリを直接使用することを選択している.