高级编程语言解释器大多采用了字节码(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 <- num
OP_ADD, //ADD dest,src1,src2 : src1 + src2 = dest
OP_SUB, //SUB dest,src1,src2 : src1 - src2 = dest
OP_MUL, //MUL dest,src1,src2 : src1 * src2 = dest
OP_DEC, //DEC dest,src1,src2 : src1 * src2 = dest
OP_OUT, //OUT
OP_STOP, //STOP
};
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编译器有两个备受诟病的扩展,就是&&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;
}
看上去实在棒棒哒,不过很可惜有两个大问题:
- 在Windows/VC++平台上,汇编版本的Indirect Threading竟然比Big Switch还要慢,并且慢很多!
- 在Windows/VC++平台上,X64编译器不支持内联汇编,要么放弃64位,要么就转去用Intel C++编译器。
其实使用GCC编译的话,如果不使用-O2优化选项,直接生成代码,得到的结果仍然是BigSwitch要快一些,在优化后,Indirect Threading的版本要比BigSwitch版本快大约10%。
内联汇编版本和GCC未优化版本之所以慢,就在那一个跳转上:
VC++最终编译后的汇编指令:
jmp DWORD PTR _addr$[ebp]
GCC开启优化后的汇编指令:
jmp *%rax
有鉴于此,一些编译器选择直接使用汇编来实现Bytecode VM,比如LuaJIT。