更多精彩文章,欢迎关注作者微信公众号:码工笔记
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到变量的技巧。
假设push
、pop
的代码都是以宏实现的,代码中还分别定义了HANDLE_PUSH
、HANDLE_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上的源码吧(见下面的参考链接)。
参考链接
- [1] WAMR介绍:www.intel.com/content/www…
- [2] Github: github.com/bytecodeall…