小型计算机字节码解释器(2007)

2022-02-22 09:09:08

我';我以前得出的结论是';在现代社会,除了为了获得更紧凑的代码外,没有什么理由使用字节码,因为字节码非常有效。那么,什么样的abytecode引擎将为您提供更紧凑的代码?

假设我想要一个字节码解释器,用于一个非常小的编程环境,特别是最小化程序所需的内存;比如,在一个32位微控制器上,有40KiB的程序闪存,程序闪存的大小通常是限制机器功能的因素。

fib:n<;2如果正确:[^1]如果错误:[^(自我谎言:n-1)+(自我谎言:n-2)]

9<;10>;pushTemp:010<;77>;推力常数:211<;B2>;发送:<;12<;99>;jumpFalse:1513<;76>;推力常数:114<;7C>;returnTop15<;70>;self16<;10>;pushTemp:017<;76>;推力常数:118<;B1>;发送:-19<;E0>;发送:fib:20<;70>;self21<;10>;pushTemp:022<;77>;推力常数:223<;B1>;发送:-24<;E0>;发送:fib:25<;B0>;发送:+26<;7C>;returnTop

或者,正如我翻译成“伪福思”的那样,";n2<;如果1返回,那么self n1-递归self n2-递归+返回";。

CPU指令集的优度指标与字节码解释器的优度指标略有不同。字节码解释器不';他们不得不担心时钟速率(因此组合逻辑路径长度)或目前为止的并行性;他们可以代表自己使用任意数量的存储;他们';你更容易修改;他们的基本操作可以利用更多的间接性。

下面是字节码解释器可以做的一些事情的例子,硬件CPU可能会遇到更多问题:

您可以拥有非常大的寄存器集(这或多或少是Squeak和#39;s VM的功能,将局部变量视为寄存器),而不会导致缓慢的过程调用和返回;MMIX还建议如何在硬件中实现这一点。

您可以想象,每个过程都可以有自己的寄存器集(可能是在SPARC上),一些指令可以访问这些寄存器的内容;再一次,吱吱';虚拟机就是这么做的

您可以有一条指令来创建一个新的抢占式调度线程,可能会在每条指令之间切换线程,就像在Core Wars或Tera MTA中一样;

如果语言是面向对象的,那么您可以使用一些指令来调用某些独特的self方法或第一个参数,如Squeak VM;

或者,作为同一事物的更一般形式,输入某些上下文可能会重新编程某些指令以执行某些任意操作;

您可以对基本的CPU操作进行各种标记测试和动态调度,比如Squeak VM、LispMs或Python';字节码;

作为基本的机器操作,您可以支持关联数组查找、附加到无限大小数组等。

我不';我手边没有FORTH,但我认为FORTH的定义是这样的:

:FIB DUP 2<;如果DROP 1 ELSE DUP 1-递归交换2-递归+然后;

我认为,以一种间接的方式,它会编译成一个字典入口,包含如下内容:

DUP(2)<;(如果)#3下降(1)(否则)#8重复1-FIB交换(2)-FIB+;

那';18个线程槽,36个字节,加上字典结构的开销,我认为对于忘记单词名的字典来说,通常是2个字节。比PowerPCassembly(96字节)好,但不是很好,明显比Squeak差。

如果我们用一个简单的Lisp解释器来解释fib,walkstree结构会怎么样?我们可以将其定义如下:

(标签fib(n)(if(<;2n)1(+(fib(-n1))(fib(-n2щщ)))

那';s 17个非括号标记和9个右括号,用于cons树上总共28个叶节点。这意味着该树包含27个conses,即内部节点中包含单元的54个内存地址,可能至少为108字节。我的结论是,虽然这种程序表示方法非常简单,但它占用了很多空间。我不';我不认为cdr编码会有足够的帮助,因为这些列表都不是很长;如果有9个列表包含25个指针和9个单字节长度或单字节终止符,那么仍然有59个字节。

根据";Lua5.0和#34;的实现;,卢亚和#39;s的虚拟机自2003年以来一直以注册为基础。他们声称他们的四条byteregister指令不是';它并没有比基于堆栈的指令多得多,部分原因可能是它们';重新比较基于tostack的指令,该指令适用于除堆栈外还具有localvariable存储的单堆栈计算机。

卢亚和#39;s基于寄存器的虚拟机相当小:";[O] n Linux及其独立解释器,配备所有标准库,不超过150 KB;核心小于100千字节" 他们';Ve之前说过,编译器的大小约为内核大小的30%,这表明内核的其余部分,包括字节码解释器,大约为70KB。

他们提到它有35条指令,几乎可以放入5位操作码中:MOVE、LOADK、LOADBOOL(转换为布尔值并有条件地跳过一条指令)、LOADNIL(清除一堆寄存器)、GETUPVAL、GETGLOBAL、GETTABLE、GETGLOBAL、SETUPVAL、SETTABLE、NEWTABLE、SELF、ADD、SUB、MUL、DIV、POW、UNM(一元减)、NOT、,CONCAT(一组寄存器的字符串串联)、JMP、EQ、LT、LE、TEST、CALL、TAILCALL、RETURN、FORLOOP、TFORLOOP、TFORPREP、SETLIST、SETLISTO、CLOSE和CLOSE。

调用将一系列寄存器传递给函数,并将其结果存储在一系列寄存器中;这意味着虚拟机不需要保存和恢复堆栈帧。本文使用了#34;注册窗口";将其与SPARC的功能进行比较。

本地a,t,i加载nil 0 2 0a=a+i添加0 0 2a=a+1添加0 0 250a=t[i]可获取0 1 2

看起来,您应该能够在两台stackmachine上将其编译为NIL NIL DUP>;R+1+R>;NIL GETTABLE是9指令而不是11指令,而且显然很愚蠢,因为NIL既不是表也不是数字。如果你真的能将其放入6字节,那么它可能比他们当前方案的12字节或之前方案的11字节有所改进。最好是编写更真实的代码片段。

本文还讨论了一个有趣的闭包实现,其中捕获的变量迁移到堆分配的结构uponfunction return中。

MuP21是在6000个晶体管中实现的,包括一个NTSC信号发生器和一个外部DRAM控制器,所以应该可以用相当少的软件来模拟其行为。这里';这是指令集:

传输指令:跳转、调用、RET、JZ、JCZ内存指令:加载、存储、LOADP、STOREP、LIT-ALU指令:COM、XOR、AND、ADD、SHL、SHR、ADDNZ寄存器指令:LOADA、STOREA、DUP、DROP、OVER、NOP

COM是补码。CPU有一个寄存器,通过LOADA和STOREA访问,为LOAD和STORE提供地址;我认为Load和STOREP也增加了它。我认为如果进位为零,JCZ会跳。(堆栈上的每个寄存器都有自己的进位;";21";表示20位内存字大小,加上额外的位。)

F21对MuP21和#39有27条指令;24岁。(只有23个是ListedBove,嗯。)它们被重命名为:

代码名称说明Forth(带有一个名为a的变量)00 else无条件跳转else 01 T0跳转如果T0-19为假,则不执行drop DUP如果02调用push PC+1到R,跳转:03 C0跳转如果T20为假进位?如果06从R返回pop PC(子程序返回);08@R+从R中的地址提取,递增R@@R>;1+>;R 09@A+从A中的地址获取,增量A@1 A+!0A#从PC+1获取,增量PC LIT 0B@A从地址获取@@0C!R+存储到R中的地址,递增R@!R>;1+>;R 0D!A+存储地址为A,增量为A@!1A+!0F!要在A@中地址的商店!10 com补码T-1 XOR 11 2*左移T,0到T0 2*12 2/右移T,T20到T19 2/13+*如果T0为真,则将S添加到T,如果超过+1,则将14-或异或S添加到T XOR 15,将S添加到T+18 pop pop R,按下T R>;19 A推A到T A@1A推T到T推1B推S到T推1C推pop T,推R>;R 1D A!跳到A!1E nop延迟2ns nop 1F下降pop T下降

T是栈顶;R是返回堆栈的顶部;S是堆栈顶部正下方的元素。我认为@R+和!R+是三条新指令中的两条;push和pop可能是另一种,因为它们是on#39;似乎不在MuP21列表中。

我';我不确定else、T0和C0指令跳转到哪里;可能是操作数堆栈上的下一个地址。

有趣的是,没有';这似乎不是一个获得";1" 在不使用#指令的情况下将其加载到堆栈上,这很烦人,因为这需要25位指令。嘟嘟嘟嘟——或者一个@A+drop是另一种30位的方法,但它会破坏A寄存器并发出无用的内存引用。dup dup或com 2*com是另一种25位方法。

所以这里';根据我有限的理解,这是我用F21代码表达的愚蠢的fib基准测试,但我并不想太聪明:

fib:dup#-2+#returnone swap c0 dup#-1+#fib call swap#-2+#fib call+;returnone:drop-drop#1;

在文字方面损失惨重;如果我们假设#立即推动其价值,而不#39;如果不需要任何NOP(例如,为了避免每个单词有多个#指令),那么我们有22条指令和7个文字——6条指令和7个文字,总共32条。5字节。不是我希望的代码密度方向!

fib:dup#-2 dup push+#returnone swap c0 dup#-1+#fib dup push call swap pop swap pop+swap call+;returnone:pop drop#1;

fib:dup#-2 dup push+#returnone swap c0 dup#-1+#fib dup push call swap pop swap pop+swap call+;returnone:drop-pop-com;

这使得它有29条指令,但只有4个文字——8个指令字,4个文字,总共12个20位字,或30字节。在尺寸上仍然比吱吱作响的版本更糟糕——而且很难读懂!有些文字可能仍然靠得太近,无法在真正的机器上工作。

如果我们改为使用三个指令16位单词,并使用高位来标记文字,我们可能会赢得更多。

fib:#-2次重复推送+nop-nop#返回一次交换c0次重复#-1次重复-nop-nop#fib-dup推送呼叫交换pop-swap-pop+swap-call+;returnone:drop-pop-com;

那';s 33指令,但四个文字不';t计数,因此29条指令或10个16位指令字加上4个16位文字。那';s 28字节,几乎与Squeak版本相同,但更糟!那';我也在努力巧妙地重新安排指令。

现在我开始明白了为什么查克·摩尔会通过这样做重复FOO二十次:FOO5 FOO FOO FOO FOO;FOO5 FOO5 FOO5 FOO5而不是使用DO循环。数字是F21上真正的痛苦!(但也许这是应该的;编程与数字无关。)

有两个堆栈就不需要局部参数向量;您可以在调用和返回堆栈之间左右移动变量,可能在执行过程中进行交换,以获得所需的值。(如果有一个";重复下一个指令四次";指令:>;R、>;R>;R、4x>;R>;、4x>;R、4x>;R>;R、4x>;R>;、4x>;R>;、4x>;R>;,等等,并且在其他方向上类似。)它不是';对于我来说,哪种方法会产生无用的代码,或者它是否取决于参数和局部变量的数量,这一点并不明显。

我想我';我在realcode中看到了发行版的样子,所以我在Squeak 3.8-6665中运行了以下代码。(毫无疑问,任何Smalltalk程序员都可以改进它。)

收集统计数据和#34;有很多临时工的方法有多普遍" | totaldict tempdict argsdict更新| tempdict:=字典新建"也许不是最好的容器" argsdict:=新字典。totaldict:=新字典。更新:=[:dict:key | dict at:key put:(1+(dict at:key ifAbsent:[0])])。Smalltalk allClassesDo:[:类|(数组中带有:类和:类类类)do:[:cl | cl选择器和方法do:[:sel:meth |更新值:tempdict值:meth numTemps.更新值:argsdict值:meth nummargs.更新值:totaldict值:meth numTemps+meth nummargs.].]{';temps';>;tempdict.>;args';>;argsdict.>;total';>;totaldict.}

#(';temps';->;字典)此外,现时有14 926 6 6 6-gt;939 9 9 9 7 7 7 7 7-gt;939 9 9 9 7 7-湾湾中中中中中中中中湾湾湾湾湾中中中中中中中湾湾湾中中中中中中中中中中湾湾中中中中中中湾中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中gt;133->;237->;139->;150->;1)和#39;args'->;词典(0->;26141->;15903 2->;4717 3->;1712 4->;756 5->;3096->;1387->;648->;379->;1310->;811->;212->;113->;1)和;总数#39->;字典此外,现时四九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九九>;530->;633->;135->;136->;138->;142->;144->;146->;162->;1 ))

那';49775种方法中的s;因此,大约95%的方法有8个或更少的参数和临时变量,90%有6个或更少,75%有3个或更少,69%有2个或更少。这表明,在像MallTalk这样的代码库中,在字节码中使用两个堆栈而不是一个局部参数向量可能是一个边际成本。

但是,可能有很多局部变量和参数的方法更长,所以在实现这些方法时效率低下可能会导致效率低下与它们的数量不成比例。这在多大程度上扭曲了结果?CompiledMethod类有initialPc和endPC方法,它们返回字节码的边界,因此我将代码更改为计算字节码而不是方法:

收集统计数据和#34;有很多临时工的方法有多普遍" | totaldict tempdict argsdict更新| tempdict:=字典新建。argsdict:=新字典。totaldict:=新字典"也许不是最好的容器" 更新:=[:dict:key:incr | dict at:key put:(incr+(dict at:key-ifAbsent:[0]))]。Smalltalk allClassesDo:[:class |(数组with:class with:class class class)do:[:cl | cl选择器和方法do:[:sel:meth | | methbytes | methbytes:=meth endPC-meth initialPC+1.更新值:tempdict值:meth numTemps值:methbytes.更新值:argsdict值:meth nummargs值:methbytes.更新值:totaldict值:meth numTemps+meth nummargs值:methbytes.]{';temps';>;tempdict.>;args';>;argsdict.>;total';>;totaldict.}

'总数#39->;字典10.0-gt;2525591 5-gt;125591 5 5-gt;92925 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7-gt;92677 7 7 7 7 7 7-gt;92677 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7-gt;748181811 1 1 1 1-gt;1718181811 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 7-gt;湾湾湾中中中中中中中中中中gt;41414141414141414141414141417 7 7 7 7 7 7 7 7 7-gt;湾湾中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中中gt;湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾湾>;5229 26->;2915 27->;3747 28->;255129->;2217 30->;301433->;12 35->;405 36->;505 38->;341 42->;357 44->;235 46->;336 62->;1028 )

其中50%是在包含4个或更少本地人和arg的上下文中定义的;60%为6岁或以下;70%为7岁或以下;80%的人10岁或以下;90%为14岁或以下;95%为27岁或更少。那';虽然原始方法并不令人鼓舞,但它仍然表明该方法是可行的,可能不需要";4x和34;说明是早些时候提出的。(即使是在一个包含14个局部变量的方法中,所有这些变量都是同时活动的,具有真正的随机访问,我认为从您当前所在的变量到您想要的变量的平均距离仅为14的三分之一,即4.7。)

也许我可以跟着MuP21';s引导并使用五位零操作数指令,用于两堆栈抽象机器。也许我应该把它们打包成5到32位的单词,或者3到16位的单词;左超位可用于标记指令流中的即时数据,如Leong、Tsang和Lee';基于CPU的MSL16 FPGA。

5位指令的吸引力在于,比方说,我的示例程序可能用不到26个字节或13个16位字来表示:39条指令或16位文字。我们能做到吗?很明显,这取决于指令集。对于示例哑斐波那契程序来说,理想的这种指令集将使其变得简单

它有11条指令,8字节,9条指令。其中一些指令--dup、swap、+和---显然会被包括在任何类似CPU的文件中;其他的——1-,return-1-if-less-than,2,2-和recurse——则不太可能。这里';sa版本,具有更可能的指令集:

dup 1交换2-负?条件返回popdup 1-literal(fib)调用交换1-1-literal(fib)调用+;

call、literal和pop也几乎肯定会存在;这个版本只额外使用1,2,-,负数?,条件返回和1-。它包含17条非文字指令和两个文字,所以如果文字是两个字节,它将是16个字节。

对于这个函数,我们没有';我真的不需要两个或更多的指示"2 -"可以像";1- 1-". 这使得所需的指令集减少到9条常规指令,再加上文字指令。

剩下的曲目中唯一可疑的指令是否定的?,它';这只是因为MuP21没有';我不知道消极性。我认为这相当于测试进位,这实际上很可能是一件非常合理的事情,要么有一个操作要测试,要么有条件返回测试。

遵循MuP21/F21模型,也许我们可以改进Squeak#39;通过避免使用特殊空间和局部变量的specialinstructions,避免使用messageargument计数(并支持多个返回值),以及可能通过将对消息选择器的引用内联到字节码中,而不是在单独的文本表中,来编写SByte代码。我的squak实例目前只有30474个不同的消息选择器,因此选择器标识符的16位可能会适应更多年的发展。

这些擦除不会以安全为代价——在Smalltalk中,方法的参数签名隐含在选择器中,只要字节码编译器没有错误,调用的任何伪方法都会弹出正确数量的参数并推送一个单一返回值。

可能堆栈操作指令(#dup over push pop nopdrop)和控制流指令(call else T0 C0 ret)会保持不变,只是增加了一个";发送";指示以某种形式保留A寄存器,作为消息的目的地,这可能也很好,这意味着保留A和A!指令,共有14条固定指令。34岁;发送";指令只需在调用过程中将对象引用保留在A中,并期望它被保留——A";一推";在击打它和a"之前进行排序;砰的一声" 返回前的顺序可能没有太多要求。

Smalltalk';s块在这种环境中可能有点困难——访问方法局部变量,从其包含的方法中进行应答,以及在self上调用方法;当然,在编译器内联控制结构的情况下,这些都不是困难。当然,把它们抽象成完整的对象是可能的

......