[讨论]VM指令分发技术:Thread Code

lurker0 2010-02-19
最简单的VM指令分发应该就是switch case做opcode dispatcher
switch(Decode(*ip++)) {
case OP_A :  op_a();
case OP_B :  op_b();
...
}
假设这个指令分发函数叫做 VmDispatcher,
那么在实际VM运行过程中就会看到执行流不停地在各个Opcode处理函数和VmDispatcher之间切换。

Thread codehttp://www.complang.tuwien.ac.at/forth/threaded-code.html http://en.wikipedia.org/wiki/Threaded_code技术带来的改进就是:
把字节码翻译成对应处理代码的地址,字节码执行的过程也就是跳转到处理代码的实际地址。
这样就没有调用过程,一条字节码运行结束后就跳转到下一条字节码处理代码的起始地址继续处理,从而节省了频繁出入堆栈的开销。
start:             thread:         pushA:  *sp++ = A
  ip = thread        &pushA                jump *ip++
  jump *ip++         &pushB        pushB:  *sp++ = B
                     &add                  jump *ip++
                     ...           add:    *sp++ = *--sp + *--sp
                                           jump *ip++


理论和实际的测试结果都证明这种方法比第一种要快很多。我的疑问在于
Thread Code在执行之前还是要做一次“查找字节码,并替换为处理代码的起始地址”这样的过程,好像总的开销并没有降下来,而且这一过程和JIT处理字节码(翻译字节码到机器码)比较类似。
另外和原始的字节码相比,也没有达到压缩代码的目的,不知道Thread Code的优势在哪里?
RednaxelaFX 2010-02-19
你所描述的是direct-threading,而threaded-code还有许多别的方式。像是说token-threading的话就没有预先处理的那一步,可以直接用原本的字节码。

如果把direct-threading看成一种“缓存”,那么要让它成为“优化”自然需要程序被执行多次才会体现出来。例如说原本用token-threading,某函数A里有10条字节码指令,每执行一条就得查一次跳转表,那么函数A被调用100次的话就查了1000次表;换成direct-threading,函数A里的10条字节码指令一开始就被逐条查表替换为实际的处理程序的地址,那么函数A被调用100次也只是最开始查了表而已,后面都不需要再查。

如果是只执行一次的程序,那肯定是最直观的最不缓存东西的执行方式最快,反正缓存了也用不上。

direct-threading比“编译”所缓存的东西较少,所以代码膨胀也较少。
lurker0 2010-02-20
那么就运行多次这种情况,如果拿direct_threading和JIT比,
同样是一次翻译,多次运行的模式,是不是可以说JIT更优?
因为JIT可以做到多字节码优化,从而产生较少的机器码,而direct_threading还是停留在单字节码解释级别。
RednaxelaFX 2010-02-20
direct-threading的预先处理也可以做成just-in-time这种lazy方式。
direct-threading与JIT编译相比,在硬件资源受限的条件下会体现出优势,因为内存本来就不够用,代码膨胀会是个重要的考虑点。
同样是因为代码膨胀小,direct-threading带来的i-cache miss(指令缓存缺失)也可能少些。把这个因素考虑进去,JIT编译并不能保证比direct-threading解释要快,必须要精心设计才行。

从启动速度看,direct-threading的预先处理时间和空间开销都不大,可以很快进入执行;JIT编译则视乎优化步骤的不同而可能有不同的时间和空间代价,中间可能涉及为临时数据结构分配和释放空间、遍历图做流分析之类的,在时间和空间上都会比direct-threading消耗更多。

direct-threading解释比JIT编译“缓存”的东西少。我觉得还是那个原理:后续重复执行次数越多,缓存就越有意义;缓存越多,初始开销就越大,但后续重复执行开销就越小。

总的来说,解释性技巧都比编译性技巧的初始开销小,适合用于执行简单的程序或程序的启动阶段。Sun HotSpot、IBM J9等JVM坚持用解释与编译结合的混合执行模式也就是为了让程序的各个部分、各个执行阶段都能得到合适的处理,发挥解释与编译各自的长处。
mikeandmore 2010-02-20
就是个computed goto么
和jit效率差远了。。。
RednaxelaFX 2010-03-03
《Language Implementation Patterns》书中9.3小节,第237页,提到这么一句,总结到要点上:
Terence Parr 写道
The more highly we process the program before execution, the faster it will go at run-time.
xuehui.hf 2010-03-24
“Thread Code在执行之前还是要做一次“查找字节码,并替换为处理代码的起始地址”这样的过程,好像总的开销并没有降下来,而且这一过程和JIT处理字节码(翻译字节码到机器码)比较类似。 ”

替换地址的操作并不是在运行阶段完成的,而是在编译阶段由编译器完成。所以并没有所谓的替换开销。这个可以参考android  dalvik的代码,dalvik\vm\meterp\out\InterpC-portstd.c。
RednaxelaFX 2010-03-24
xuehui.hf 写道
“Thread Code在执行之前还是要做一次“查找字节码,并替换为处理代码的起始地址”这样的过程,好像总的开销并没有降下来,而且这一过程和JIT处理字节码(翻译字节码到机器码)比较类似。 ”

替换地址的操作并不是在运行阶段完成的,而是在编译阶段由编译器完成。所以并没有所谓的替换开销。这个可以参考android  dalvik的代码,dalvik\vm\meterp\out\InterpC-portstd.c。

这个有不同的做法,替换步骤可以在编译时做也可以在VM里刚加载代码的时候做。后者的话可以参考一些论文,例如这篇开头的背景介绍:Optimizing direct threaded code by selective inlining
怎么做合适是取决于应用场景的。
xuehui.hf 2010-04-13
RednaxelaFX 写道
xuehui.hf 写道
“Thread Code在执行之前还是要做一次“查找字节码,并替换为处理代码的起始地址”这样的过程,好像总的开销并没有降下来,而且这一过程和JIT处理字节码(翻译字节码到机器码)比较类似。 ”

替换地址的操作并不是在运行阶段完成的,而是在编译阶段由编译器完成。所以并没有所谓的替换开销。这个可以参考android  dalvik的代码,dalvik\vm\meterp\out\InterpC-portstd.c。

这个有不同的做法,替换步骤可以在编译时做也可以在VM里刚加载代码的时候做。后者的话可以参考一些论文,例如这篇开头的背景介绍:Optimizing direct threaded code by selective inlining
怎么做合适是取决于应用场景的。


呵呵,谢谢你的推荐,很久没看论文了,需要好好充实充实了
RednaxelaFX 2010-06-17
顺带加个例子:
GCJ的解释器中,void _Jv_InterpMethod::compile (const void * const *insn_targets)方法,就是在运行的时候把字节码转换为direct-threading代码的。
Global site tag (gtag.js) - Google Analytics