WAMR虚拟机性能三驾马车之——字节码快速派发

172 阅读4分钟

更多精彩文章,欢迎关注作者微信公众号:码工笔记

WAMR 简介

WAMR(WebAssembly Micro Runtime)是一款开源的WebAssembly虚拟机,它体积小、性能高,适用于IoT、云原生等各种场景。它采用栈机指令集,支持解释执行、JIT等多种运行模式。

解释执行的性能方面,根据[1]的介绍,它主要做了三方面的优化:

  • 字节码快速派发(Fast Bytecode Dispatching)
  • 字节码融合(Bytecode Fusion)
  • 代码段中的整数预解码

本文主要讲字节码快速派发。

字节码快速派发

了解过虚拟机解释执行流程的同学都知道,最简单的虚拟机解释执行字节码的核心逻辑就是一个for循环:

  • 读入一条字节码指令(栈机上如push、add、pop等),
  • 根据指令操作符名称/类型跳转到对应的处理代码中,
  • 然后再读下一条指令,循环往复。

其中,实现跳转最简单的方式是一个大switch语句,类似下面这样:

//0. 字节码文件已经加载到内存中,现在要解释执行它了,pMem指向字节码中的代码段部分

//1. 从内存中读入字节码指令操作符,pMem指针后移
auto operation = *((OP_TYPE *)pMem++);

//2. 根据不同的指令操作符,跳转到不同的处理函数
switch (operation) {
case NOP:
   break;
case PUSH:
   handle_push();
   break;
case POP:
   handle_pop();
   break;
case ADD:
   handle_add();
   break;
...   
}

一个普通方法中上百条指令很正常,每条指令都要走这段逻辑,所以这里是超级热点。

如果每次都调switch让流水线预判分支(因为相邻指令之间的随机性,很可能不会太准)一定会拖慢性能。

一个常见的优化手段是建立一个跳转表,如上述代码可改为:

//先定义好一个函数指针数组,排列顺序与op的值相同,使后续能以op为下标取出正确的handler
OP_HANDLER_TYPE op_handlers[] = {
   &handle_nop,
   &handle_push,
   &handle_pop,
   ...
}

//取指令
auto operation = *((OP_HANDLER_TYPE *)pMem++);

//以operation为下标找到对应的处理函数进行调用
op_handlers[operation](pMem);

这样就能省去分支判断逻辑,最大化CPU流水线的利用率。

但这种方式下,每次还是要先从op_handlers中取出处理函数的指针,再进行调用,需要一次访存操作(内存操作相对于CPU的运行速度来说是很慢的),能不能把这次访存也优化掉呢?

重点终于来了

WAMR的字节码快速派发做的就是这个事:

WAMR会在加载字节码文件到内存时(低频操作,一个文件只加载一次),将字节码中填指令操作符(operation)的位置替换成其对应的处理函数的地址,那么后续在解释执行指令(高频操作,一个文件中的指令会被执行N次)时,就变成了下面的流程:

//字节码中存放的已经是处理函数地址
auto handler = *((OP_HANDLER_TYPE *)pMem++);

//直接调用handler即可
handler(pMem);

当然,因为这段逻辑是虚拟机最核心的部分,其执行次数非常之多,所以这部分一般不会写成函数(函数调用是有开销的),而是用宏包一下。

另外,这个跳转还使用了goto语句和存储label到变量的技巧。

假设pushpop的代码都是以宏实现的,代码中还分别定义了HANDLE_PUSHHANDLE_POP的label:

//这里是指令的具体实现
#define PUSH_PROC()  ...
#define POP_PROC()   ...
...

//以下为所有指令的跳转入口

HANDLE_NOP: {
 NOP_PROC() //处理NOP
}

HANDLE_PUSH: {
 PUSH_PROC() //处理push的逻辑
}

HANDLE_POP: {
  POP_PROC() //处理pop的逻辑
}  
...

//op_hanndlers数组中存放所有指令的跳转label(注意&&语法)
op_handlers[] = {
   &&HANDLE_NOP,
   &&HANDLE_PUSH,
   &&HANDLE_POP,
}

注意,这里HANDLE_XXX是自定义的C语言label(可以goto的那种),op_handlers的初始化中用了&&label的语法,它可以保存label到一个变量中,让后续代码通过goto语名跳转到此变量中存放的位置

在加载字节码文件到内存后可以将字节码中存指令的地方(原来存operation)直接替换成 op_handlers[operation]。(这里得注意长度)

然后,在解释执行这段字节码时,快速派发的逻辑就可以这样实现:

//字节码中存放的是:&&label
auto handler = *((OP_HANDLER *)pMem++);

//这里handler已经是指向当前进程代码段的真实地址了,可以直接goto
goto handler;

当然,还要注意goto语句不像调用方法一样执行完还能返回当前代码位置,而是需要在PUSH_PROC()等宏的尾部增加跳回逻辑(跳到下一条取指令的位置),以保证流程能继续下去。

今天就先写到这儿了,末尾声明下,上面的伪代码并不是WAMR的源码哈,只是为了能讲清楚原理瞎写的,大家要了解真正的工程实现,还是看github上的源码吧(见下面的参考链接)。

参考链接