从宿主调用脚本函数开始说起。宿主调用一个C++接口函数来执行指定的脚本函数,接口函数里有一个栈优先的堆栈类的临时变量,还有一些循环执行指令列表所需要的临时变量,比如当前指令地址(通常叫pc),比如记录指令数据变化类的临时变量,还有很多为了优化执行而设计的变量。
执行过程如:宿主调用>>call func1>>call func2>>call func3>>call func4>>return func3>>return func2>>return func1>>return 宿主调用
每一次的脚本函数调用,都需要如下操作:
- 申请一块内存空间,以便于存储input和temp的数据。然后被调用者引用这块空间。填如output的地址到temp里
- 根据函数信息,将编译期明确好的参数,把需要的数据查找到,然后填充到temp里面。比如函数里面有常量,那么就把常量属性表的数据地址存到对应的temp位置,这个位置就是编译期明确好的参数。另外还要预留当前运行的指令位置数据,即pc值。还有当前函数所绑定的属性集的数据地址。
- 写入input数据到那块内存空间。
- 根据pc值,在while循环里读取指令,然后执行指令
- 当call函数时,那么这块内存空间先保留,重复1
- 函数结束前,根据output的地址,填写对应输出,最终执行return指令
- return指令并不是跳出循环,而是释放(归还)所申请的内存空间,然后根据上一个函数的pc等重置循环参数。接着就是重复4
说了这么多,那么什么是函数运行体呢?它其实就是一个方法,它必须有一个管理所申请的内存空间的类(寄存器类),除了寄存器,它还需要引用指令列表,还需要在函数挂起(协程的挂起)时,把栈优先的堆栈存起来,当函数恢复(协程的继续执行)时,取替当前的栈优先的堆栈变量,然后继续第4步。每次执行的时候都应该进行记录(用于调试或者dump后的查看)。为什么不是一个具体的类?抽象起来还是有点难度的,但更多的是为了效率,就一个循环函数的事,还抽象个啥?
明确一下函数运行体的概念:执行某个脚本函数的过程,所涉及到的寄存器,记录器,指令列表,当前循环所需的临时变量等,它们之间以某种优化的搭配方式组合起来,在面向各种各样的情况下(如挂起,恢复,调试,异常等),如何确保脚本的正确执行的方法叫函数运行体。简单来说,确保脚本高效正确运行的方法就是函数运行体。
根据这个概念,帮助我们设计理解那个C++接口函数的具体实现。
上述的执行过程可以用函数运行体描述:
大家是否发现,整个执行过程,其实很像先入后出的执行过程?call为入栈,return为出栈!
问题来了,如何设计这个函数运行体?
Lua开始的版本是使用栈操作实现,基本操作就是入栈/出栈/改变栈数据。后来引入了寄存器的设计,数据存于寄存器,指令只存寄存器的地址相对偏移量。目前最新的lua是存在2种指令集的。
而我设计的是基于寄存器和属性集的函数运行体。最终目的是修改属性集的属性。寄存器只是为了提高执行效率而设计的,因为属性集的属性读写相对寄存器都是比较高成本的。虽然大家都是一段连续的内存,但属性集都在堆上,而寄存器大部分都在栈上,另外属性集设计比较复杂,某个属性变化还要触发一堆事件。属性集还需要考虑动态扩展属性等等。寄存器和属性集只是说明了内存的布局,那么具体怎么执行,还要看指令集的设计。
举个例子:有变量a、b、c,执行a = b + c 说明一下,这里没有涉及到变量的类型,假设都为int
在lua中就是R(n1) = R(n2)+R(n3), 其中R指寄存器,nx指对应下标
在我这就要分裂成3种加法指令:
- a,b,c都在寄存器R上,那么就是R(n1) = R(n2)+R(n3)
- a是output,其他在寄存器R上,那么就是OutR(n1) = R(n2)+R(n3),其中OutR表示上一个函数体的寄存器R
- 都是属性集(包括表格T)里的:A(n1) = A(n2)+A(n3),还记得函数运行体执行时的第2步最后的一句话吗?“还有当前函数所绑定的属性集的数据地址。”在R的temp里是有个固定长度的指针数组,里面动态的存放A,T等数据地址。A与T的关系具体看属性集的设计.
如果考虑到类型,而且算术运算只考虑int,double,int128,那么如果再展开的话,一条a=b+c的加法要分裂成9条指令!!!这就是展开的代价。也是本设计的最大缺点。当然有办法优化,那就是二级指令!具体看指令的设计。Lua的算术指令是14条,lua的一条指令被压缩成一个int32的数值,内存是少了,但是我的设计是加载后,把指令展开,空间换时间,这点内存的牺牲是很值的。另外就是指令放在栈上面就更快?栈和堆的读取其实是差不多的!!!所以展开放在堆上没毛病!!!写入就不同,所以才需要寄存器。