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

本文探讨如何使用JavaScript构建一个字节码解释器,特别是用于重新实现JS科学计算器的后端。文章介绍了优先级、结合性和求值顺序的区别,并讨论了基于栈的字节码指令与基于寄存器的指令架构的优缺点。作者通过比较不同表达式的求值过程,阐述了栈帧结构和局部变量区的作用,并提到了Dalvik VM和x86寄存器模型的差异。
摘要由CSDN通过智能技术生成

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


https://codereview.chromium.org/1485023004/diff/20001/src/mips64/simulator-mips64.cc

呃。。。所谓的”字节码解释器“似乎应该叫做simulator(仿真器)或者像JVM那样,”虚拟机“

http://rednaxelafx.iteye.com/blog/492667

优先级、结合性与求值顺序

这三个是不同的概念,却经常被混淆。通过AST来看就很容易理解:(假设源码是从左到右输入的)

所谓优先级,就是不同操作相邻出现时,AST节点与根的距离的关系。优先级高的操作会更远离根,优先级低的操作会更接近根。为什么?因为整棵AST是以后序遍历求值的,显然节点离根越远就越早被求值。

所谓结合性,就是当同类操作相邻出现时,操作的先后顺序同AST节点与根的距离的关系。如果是左结合,则先出现的操作对应的AST节点比后出现的操作的节点离根更远;换句话说,先出现的节点会是后出现节点的子节点。

所谓求值顺序,就是在遍历子节点时的顺序。对二元运算对应的节点来说,先遍历左子节点再遍历右子节点就是左结合,反之则是右结合。

这三个概念与运算的联系都很紧密,但实际描述的是不同的关系。前两者是解析器根据语法生成AST时就已经决定好的,后者则是解释执行或者生成代码而去遍历AST时决定的。

在没有副作用的环境中,给定优先级与结合性,则无论求值顺序是怎样的都能得到同样的结果;而在有副作用的环境中,求值顺序会影响结果。 

每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其它一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot。求值栈用于保存求值的中间结果和调用别的方法的参数等。两者都以字长(32位的字)为单位,每个slot可以保存byte、short、char、int、float、reference和returnAddress等长度小于或等于32位的类型的数据;相邻两项可用于保存long和double类型的数据。每个方法所需要的局部变量区与求值栈大小都能够在编译时确定,并且记录在.class文件里。 

嗯,引入单独的”求值栈“概念有个好处:就是一来表达式求值可以使用基于栈的字节码指令(参数Pop,结果Push),二来理论上可以分配多个栈?

啊!我第一版编译器的实现里,CallPrimitiveFunction采用了基于寄存器的字节码指令架构,自己在编译器生成指令时手工Push/Pop,难怪会导致思路不清晰。。。

如果CallPrimitiveFunction改为采用基于栈的字节码指令架构的话,由于当前不支持用户自定义函数,需要根据参数的个数拆分为2个指令:

1、CallUnaryPrimitiveFunction
2、CallBinaryPrimitiveFunction

我当初就想这么做的,但是基于栈的字节码指令解决不了常量在什么时机Push进求值栈的问题。这跟逆波兰表达法有关。

考虑表达式:1*(2-(3+4))
如果是基于栈的字节码指令的话,应该是:

PushImm 1
PushImm 2
PushImm 3
PushImm 4
CallBinaryPrimitiveFunction ‘+’
CallBinaryPrimitiveFunction ‘-’
CallBinaryPrimitiveFunction ‘*’

嗯???似乎操作数一开始已经全部压栈了??这个感觉挺简单的。再看现在的基于寄存器及ABI(R0,R1-->R0)的:

PushImm 1
PushImm 2
MovImm #3, R0
MovImm #4, R1
CallPrimitiveFunction '+'
Mov R0, R1
Pop R0
CallPrimitiveFunction '-'
Mov R0, R1
Pop R0
CallPrimitiveFunction '*'

编译器的代码生成逻辑明显更复杂!

上面的例子给人一种错觉,以为基于栈的字节码指令就是一开始把表达式中的所有操作数依次压栈(假设所有运算都是左结合的,不考虑括号带来的优先级提升),实际上不是这样,考虑下面的表达式:

(1+2)*(3-4)

对应的栈虚拟机指令:

PushImm 1
PushImm 2
CallBinaryPrimitiveFunction ‘+’
PushImm 3
PushImm 4
CallBinaryPrimitiveFunction ‘-’
CallBinaryPrimitiveFunction ‘*’

对应的基于寄存器的:

MovImm #1, R0
MovImm #2, R1
CallPrimitiveFunction '+'
Push R0
MovImm #3, R0
MovImm #4, R1
CallPrimitiveFunction '-'
Push R0(此处可以优化,避免不必要的Push/Pop)
Pop R1
Pop R0
CallPrimitiveFunction '*'

问题:一般基于栈的字节码指令都是虚拟机(如JVM),而实际的硬件CPU指令都是基于寄存器的,如何将前者自动翻译为后者 (传统的意义上的JIT过程)


题外话1:Dalvik VM是基于寄存器的,x86也是基于寄存器的,但两者的“寄存器”却相当不同:前者的寄存器是每个方法被调用时都有自己一组私有的,后者的寄存器则是全局的。也就是说,概念上Dalvik VM字节码中不用担心保护寄存器的问题,某个方法在调用了别的方法返回过来后自己的寄存器的值肯定跟调用前一样。而x86程序在调用函数时要考虑清楚calling convention,调用方在调用前要不要保护某些寄存器的当前状态,还是说被调用方会处理好这些问题,麻烦事不少。Dalvik VM这种虚拟寄存器让人想起一些实际处理器的“寄存器窗口”,例如SPARC的Register Windows也是保证每个函数都觉得自己有“私有的一组寄存器”,减轻了在代码里处理寄存器保护的麻烦——扔给硬件和操作系统解决了。IA-64也有寄存器窗口的概念。 

哦!~~~ 明白了

好吧,推荐一下这个博客:http://rednaxelafx.iteye.com/blog/362738




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值