当前位置: 首页 > 文档资料 > larva 中文文档 >

threaded-code 和指令预测

优质
小牛编辑
132浏览
2023-12-01

在解释字节码执行的虚拟机中,一般都会有上一篇说的switch结构,而字节码的类型显然不止前面列出的那些,一个很简单的语言都可能有上百个字节码,于是引入了另一个问题,假设用C或C++实现,在很多编译器中,switch实际会被编译成一连串的if...else if... ... ...else,执行的时候,每条指令都从头开始判断,执行某些指令时,需要成百甚至上千的比较次数,严重影响运行效率。当然这个问题并非是虚拟机解释器独有,而是switch在特定编译器的问题,比方说我们写一个网络处理程序,根据协议号分发处理,如果协议号很多,也是一样的情况。但是这个问题在解释字节码的虚拟机上特别显著,一方面因为字节码很多,另一方面因为每个字节码的执行是很简单的,相对的大量操作就浪费在了这个判断上

2012年我做了个很简单的语言,解释字节码执行,就碰到了这个问题,尽管字节码并不多,但对效率影响比较明显,当时想了一个笨办法:

switch (code >> 4)   
{   
    case CODE_GROUP_1:   
    {   
        switch (code)   
        {   
            case CODE_LOAD:   
            ...   
        }   
    }   
    case CODE_GROUP_2:   
    {   
        ...   
    }   
    ...   
}

就是将字节码分组,然后每条指令至少要两次判断,但是平均判断次数降低了,当然分组组数和每组数量要设计,上面code >> 4只是个例子

其实很多人都知道怎么解决这个问题,用一个hash表即可,比如python做服务器的时候,可能会根据协议到不同的分支:

protocol_process_map = {PROTOCOL_A : process_a,   
                        PROTOCOL_B : process_b,   
                        ...}

处理协议的时候,从表中找到函数,直接调用即可,无论多少协议,时间是O(1)

但是,如果在字节码处理上使用这种方式,则每个字节码都会进行一次函数调用,而相对于每个字节码的操作的简单性来说,函数调用还是很费时,于是就引入了threaded code技术,具体方法是,将字节码指令列表用对应的需要跳转的地址替代,然后一个个jmp过去,例(转自wikipedia):

start:   
  ip = &thread   
top:   
  jump *ip++   
thread:   
  &pushA   
  &pushB   
  &add   
  ...   
pushA:   
  *sp++ = A   
  jump top   
pushB:   
  *sp++ = B   
  jump top   
add:   
  *sp++ = *--sp + *--sp   
  jump top

这个处理的字节码是:

pushA   
pushB   
add

thread是字节码转换成的一个jmp序列,ip是索引,按照thread表里面的地址依次跳到对应位置执行即可

不过,这个技术也就字节码能用,因为字节码是确定的,预先编译(这也是编译,虽然过程简单)成地址表是可行的,但对于一般的switch来说,需要用上述hash表的方式,做成表驱动,假如各case的值范围不大,连续性也比较好,做成一个地址数组用switch条件做索引即可,否则需要生成一个hash表,为效率考虑,hash函数需要尽量简单,冲突也要尽量少,可能会浪费空间来减少冲突 P.S.对于N个不同的数据,放在容量为N的hash表中且无hash冲突,是可能的,这就是perfect-hash,但这需要利用计算机来找一个合适的hash函数,算法比较复杂,而且找出来的函数可能运算比较慢(当然也是O(1)的)

虽然已经有了这个编译技术,但并非所有编译器支持,ruby在1.9版本开始解释字节码执行,就立刻使用了这种技术,1.8之前是解释AST执行,但python从很早就解释字节码实现了,在没有threaded code的情况下,python采用了指令预测的技术来减少switch的消耗

考虑switch的if...else if... ... ...else实现,假设各条件的出现是平均几率随机的,那这显然是一个O(N)的顺序查找过程,但考虑实际情况,字节码的出现几率一般是不平均的,例如栈虚拟机中load操作数量明显大大高于其他,而运算中,加减运算比幂次(如python的**)出现几率也高很多,普通计算程序可能根本不会用到类似异或这种运算(加密算法用的比较多),于是,如果我们把常见的指令放在switch的前面,就能提高效率,一个精心设计过case顺序的虚拟机甚至可能比一个糟糕的顺序效率高20%

某些指令如load,其出现的频繁程度是可想而知的,但并非所有字节码都这么明显,为了达到一个好的顺序,比较好的办法是采用数据挖掘,统计的方法。在有了编译器后,可以写一批代码(常用算法,经典benchmark之类),编译后统计各个指令的频率,即可得到一个较好的顺序,然后可以将虚拟机发布出去给大家使用,这时候至少不会太差,然后收集实际使用中能拿到的代码(当然是合法途径拿到的,不要像某些软件一样偷别人代码),继续统计和优化调整

上面这个办法只是基于单条指令频率的预测,将可能出现的放在前面,python还实现了指令间关联的预测。可以注意到,字节码指令数量明显比源代码语句数量要多很多,这是因为源代码的一条stmt会转成多条字节码,而很多情况下一条指令转成字节码是有固定格式的,比如,我们经常写if的时候,后面跟的是判断表达式(<,>,==之类),而if在算完表达式后下一条就是pop_jmp_if_false,因此: case CODE_EQUAL: //==运算 { ...//计算过程 //假设idx已经++过了,指向下一条指令 if (inst_list[idx].code == CODE_POP_JMP_IF_FALSE) { goto do_pop_jmp_if_false; } continue; } 这个预测在失败的时候会浪费一次判断,但是成功的时候可以节省很多次判断,如果90%以上的情况下都预测成功,提速会非常明显

显然我们需要一张指令前后关系的统计表,使用上面的统计方法即可,某些指令可能有多条后继指令,可以做多次预测,但预测数量太多反而会造成性能下降,需要一个权衡