字节码设计:第一版(仅仅用于表达式计算)
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的打算,因此对于优先级和结合律问题,体现在递归下降分析过程中就是先循环还是递归的问题。