使用JavaScript实现一个“字节码解释器”,并用它重新实现JS科学计算器的后端(后续1)

来源:互联网 发布:安卓改iphone6在线软件 编辑:程序博客网 时间:2024/04/30 10:27

字节码设计:第一版(仅仅用于表达式计算)

PushImm 123
Push R2
Pop R0
Mov src, dst #寄存器到寄存器
MovImm imm, reg #加载立即数到寄存器
CallPrimitiveFunction ‘+’ #ABI: 最多2个输入参数,R0,R1,输出结果在R0

核心JS代码(部分,CSDN博客不支持上传附件):

Assembler.prototype = {  emitInstruction: function(inst) {    //应用窥孔优化:如果当前指令是Pop,并且前一条指令是Push,且寄存器参数相同,则去除这一对指令    if (inst.type=="Pop") {      if (this.code_buffer.length>0) {        var last_inst = this.code_buffer[this.code_buffer.length-1];        if (last_inst.type=="Push" && inst.arg==last_inst.arg) {          this.code_buffer.pop();          return;        }      }    }    this.code_buffer.push(inst);  },  getResult: function() {    return this.code_buffer;  },  toString: function() {    function inst2str(inst) {      return inst.type + " " + inst.arg         + (!!inst.arg1 ? " "+inst.arg1 : "");      //当前,由于设计简化的缘故,每个字节码指令最多只有1个参数    }    return this.code_buffer.map(function(inst){      return inst2str(inst);    }).join("\n");  }}

function BytecodeIntercepter() {  this.registers = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; //0~15 ?  this.local_vars = []; //this is stack, we currently don't support nested call stack;  //this.sp = 0; //stack pointer; next pos to push    //如果我们需要Push/Pop以外的直接访问特定局部变量的话,则需要sp,否则local_vars本身就够了    //猜想:如果局部变量不会被重新赋值,即满足SSA,那么Push/Pop应该就够用了,?}BytecodeIntercepter.prototype = {  mapPrimitiveFunctionName2FunctionObject: function(name){    switch(name.toLowerCase()){      case '+': return function(a,b){return a+b};      case '-': return function(a,b){return a-b};      case '*': return function(a,b){return a*b};      case '/': return function(a,b){return a/b};      case 'sin': return function(a){return Math.sin(a)};      case 'cos': return function(a){return Math.cos(a)};      case 'tan': return function(a){return Math.tan(a)};      case 'cot': return function(a){return 1/Math.tan(a)};      case 'sqrt': return function(a){return Math.sqrt(a)};      case 'log': return function(a){return Math.log(a)};      case 'pow': return function(a,b){return Math.pow(a,b)};      default: throw "未识别的原语函数:"+name;    }  },  eval: function (code_buffer) {    for(var i=0; i<code_buffer.length; ++i) {      var inst = code_buffer[i];      switch(inst.type) {        case "PushImm":          this.local_vars.push(inst.arg);          break;        case "Push":          var reg_index = Number(inst.arg.substring(1));          assert(reg_index>=0 && reg_index<16);          this.local_vars.push(this.registers[reg_index]);          break;        case "Pop":          assert(this.local_vars.length>0);          var stack_top_localvar = this.local_vars.pop();          var reg_index = Number(inst.arg.substring(1));          assert(reg_index>=0 && reg_index<16);          this.registers[reg_index] = stack_top_localvar;          break;        case "MovImm":          var imm = inst.arg;          var reg_index = Number(inst.arg1.substring(1));          assert(reg_index>=0 && reg_index<16);          this.registers[reg_index] = imm;          break;        case "Mov":          var reg_index = Number(inst.arg.substring(1));          assert(reg_index>=0 && reg_index<16);          var reg_index1 = Number(inst.arg1.substring(1));          assert(reg_index1>=0 && reg_index1<16);          this.registers[reg_index1] = this.registers[reg_index];          break;        case "CallPrimitiveFunction":          //ABI: 原始函数接受至多2个寄存器输入,对应R0、R1,计算结果放在R0里          var arg1 = this.registers[0];          var arg2 = this.registers[1];//这里利用JS的一个特性简化代码!          var func = this.mapPrimitiveFunctionName2FunctionObject(inst.arg);          var result = func(arg1, arg2);          this.registers[0] = result;          break;        default:          throw "不支持的字节码指令类型:"+inst.type;      }    }    return this.registers[0]; //最后结果在R0里  }}


编译器+字节码解释器思路的第一个版本实现,之前的测试用例都能通过,但是新的case出错了:

sin(1+2)+cos(3-4)-tan(5*6)

虽然当初我设想可以使用16个通用寄存器,但是真实现起来,才发现只用到R0 R1两个,并且我甚至把+-*/也当原语函数来实现的。

这里的原语函数把访问栈上的局部变量,只使用寄存器,感觉都有点像汇编里的宏或伪指令了。

嗯,是不是如果利用更多的寄存器,就是“lowering”?近来看v8 intercepter项目有类似提交。

另外,编译器与直接AST解释器的一个最大区别可以认为是:对局部变量不再有名字查找索引,而是相对于frame或sp的索引访问。这似乎就是“Context/Slot”的意思。

传统意义上,解释器的输入一般是源代码经过parse后的AST,对LISP语言而言,这个parse也省掉了。

但是假如先把代码/AST编译为bytecode,再对bytecode进行解释执行,这个思路就比较先进。Firefox的JS Monkey,以及QEMU,都使用了这种思路。

在这个层次上,字节码如何设计,可以节省Assembler生成的指令数;以及能否把某些解释执行进一步优化为机器指令的raw方式执行,是性能的关键。

现在,我虽然假设有16个通用寄存器,但实际上只用到R0 R1,导致了大量的Push/Pop操作,极大地浪费了不必要的字节码指令空间数目。

但是关键是先把它做对,然后再考虑进一步优化的问题。

进一步的考虑:
0、支持lowering,即考虑寄存器分配的问题,而不是一律将CallPrimitiveFunction的结果Push保存到栈上;
1、让表达式中支持变量,即允许子表达式赋值,以及后面的引用,子表达式之间用逗号或分号分隔;比如:var a=sin(1+2), cos(a*3)
   之所以可以用逗号分隔子表达式,是科学计算器里二元函数是中缀形式,如:3 pow 5
2、允许用户自定义函数(UDF),这个特性是一大跳跃,带来必须支持嵌套call stack的设想,当然,也许可以在生成字节码的过程中对UDF应用cps转换,这样可能就没有嵌套call stack了?


PS:在LLVM的术语中,lowering指的是用简单指令的组合来实现它不支持的高级指令。这跟我说的把栈上局部变量传参改为寄存器以提高性能不是一回事。

PS2:原来的代码并没有把表达式解析为AST的打算,因此对于优先级和结合律问题,体现在递归下降分析过程中就是先循环还是递归的问题。



0 0