实时(JIT)代码生成是实现编程语言时的一个重要策略。在运行时生成代码允许程序针对其运行所针对的特定数据进行专门化。对于实现编程语言的程序,这种专门化是关于正在运行的程序的,并且可能是关于程序使用的数据的。
这通常的工作方式是,程序为其运行的机器的指令集生成字节,然后将控制权传递给这些指令。
通常,程序必须将生成的代码放在专门标记为可执行的内存中。但是,WebAssembly中缺少此功能。那么,如何在WebAssembly中进行实时编译?
作为harvard体系结构的webassembly
在冯·诺伊曼机器中,就像您可能正在阅读的机器一样,代码和数据共享一个地址空间。只有一种指针,它可以指向任何东西:实现sin函数的字节、数字42、“饼干”中的字符,或者任何东西。WebAssembly的不同之处在于其代码在运行时不可寻址。WebAssembly模块中的函数从0开始按顺序编号,WebAssemblyCall指令将被调用方作为立即参数。
因此,要将代码添加到WebAssembly程序中,您必须以某种方式使用更多函数来扩充程序。让我们假设我们将以某种方式实现这一点——您的WebAssembly模块有N个函数,现在将有N+1个函数,而函数N是您的程序生成的新函数。我们怎么称呼它?如果调用指令对被调用方进行硬编码,现有函数0到N-1将不会调用它。
这里的答案是call_indirect。有一点提醒,该指令将被调用方作为操作数,而不是立即参数,允许它在运行时选择被调用方函数。被调用操作数是函数表的索引。传统上,表0被称为间接函数表,因为它包含每个函数的一个条目,这些函数可能曾经是间接调用的目标。
考虑到这一点,我们的问题有两部分:(1)如何用新函数扩充WebAssembly模块,以及(2)如何让原始模块调用新代码。
辅助webassembly模块的后期链接
这里的关键思想是,要添加代码,主程序应该生成一个包含该代码的新WebAssembly模块。然后,我们运行一个链接阶段,将新代码实际应用并使其可用。
像ld这样的系统链接器通常需要一组完整的符号和重新定位来解析归档间引用。然而,当执行JIT生成的代码的后期链接时,我们可以采取一条捷径:主程序可以将内存地址直接嵌入到它生成的代码中。因此,生成的模块将从主模块导入内存。从生成的代码到主模块的所有引用都可以以这种方式直接嵌入。
生成的模块还将从主模块导入间接函数表。(我们将确保主模块通过工具链导出其内存和间接函数表。)当主模块生成生成的模块时,它还将在生成的模块中嵌入一个特殊的补丁函数。此函数将新函数添加到主模块的间接函数表中,并在主模块内存中执行任何重定位。从主模块到生成函数的所有引用都通过补丁函数安装。
我们计划实现两种后期链接,但都共享生成的带有补丁函数的WebAssembly模块的基本机制。
通过运行时动态链接
链接器的一个实现是主模块使运行时动态实例化新的WebAssembly模块。当实例化生成的模块时,运行时将从主模块提供内存和间接函数表作为导入。
动态链接的优势在于,它可以更新实时WebAssembly模块,而无需重新实例化或特殊的运行时检查点支持。
在web环境中,JIT编译可以由所讨论的WebAssembly模块通过调用JavaScript中的功能来触发,或者我们可以使用“基于拉”的模型来允许JavaScript主机轮询WebAssemby实例以查找任何挂起的JIT代码。
对于WASI部署,您需要来自主机的功能。要么导入提供运行时JIT功能的模块,要么依赖主机轮询数据。
通过wizer的静态链接
另一个想法是利用Wizer的能力来拍摄WebAssembly模块的快照。您可以扩展Wizer,使其也能够用新代码扩充模块。在这个角色中,Wizer实际上是一个后期链接器,在新的存档中链接到现有的对象。
Wizer已经需要实例化WebAssembly模块并运行其代码的能力。让Wizer询问模块是否有任何生成的辅助模块,这些模块应该被实例化、修补并合并到主模块中,这应该不是什么大问题。Wizer已经可以运行修补功能,以执行重新定位,以修补对新功能的访问。完成之后,Wizer(或其他工具)将需要像往常一样对模块进行快照,但也需要添加额外的代码。
作为技术细节,在最简单的情况下,代码是以不直接调用彼此的函数为单位生成的,这与将函数附加到代码部分,然后将生成的元素段附加到主模块的元素段一样简单,通过在新模块连接到每个函数引用之前添加模块中的函数总数,将附加的函数引用更新为它们的新值。
延迟链接似乎是异步代码生成
从主程序的角度来看,通过后期链接生成WebAssembly JIT代码与自动生成代码相同。
例如,以C程序为例:
struct Value;
struct Func {
struct Expr *body;
void *jitCode;
};
void recordJitCandidate(struct Func *func);
uint8_t* flushJitCode(); // Call to actually generate JIT code.
struct Value* interpretCall(struct Expr *body,
struct Value *arg);
struct Value* call(struct Func *func,
struct Value* val) {
if (func->jitCode) {
struct Value* (*f)(struct Value*) = jitCode;
return f(val);
} else {
recordJitCandidate(func);
return interpretCall(func->body, val);
}
}
在这里,C程序允许生成JIT代码:Func实例中有一个插槽,可以用代码指针填充。如果该程序为给定的Func生成代码,它将无法填充指针——它无法向图像中添加新代码。但是,它可以告诉Wizer这样做,Wizer可以快照程序,链接到新函数中,并修补&func->jitCode。从程序的角度来看,代码似乎是异步可用的。
演示!
这么多的话,对吧?让我们看看一些代码!作为其他JIT编译器工作的草图,我以WebAssembly为目标实现了一个小的Scheme解释器和JIT编译器。见interp。抄送来源。您可以这样编译它:
$ /opt/wasi-sdk/bin/clang++ -O2 -Wall \
-mexec-model=reactor \
-Wl,--growable-table \
-Wl,--export-table \
-DLIBRARY=1 \
-fno-exceptions \
interp.cc -o interplib.wasm
这里我们使用WASI SDK进行编译。我有第14版。
—mexec model=reactor参数意味着这个WASI模块不仅仅是一次运行,之后它的状态被破坏;相反,它是一个多条目组件。
两个-Wl选项告诉链接器导出间接函数表,并允许JIT模块扩展间接函数表。
interp使用-DLIBRARY=1。复写的副本;您实际上可以在本地运行和调试它,但这只是为了开发。相反,我们编译为wasm,并在WASI环境中运行,为我们提供了fprintf和其他调试细节。
—fno异常是因为WASI目前不支持异常。而且我们不需要它们。
WASI主要用于非浏览器用例,但这个模块做得太少,所以它不需要WASI提供太多信息,我可以在浏览器JavaScript中对其进行填充。这就是我们这里的内容:
现场wasm jit演示
> 1
Evaluate
> ((lambda (n) (+ n 42)) 27)
Evaluate
>
(letrec ((fac (lambda (n)
(if (eq? n 0) 1 (* n (fac (- n 1)))))))
(fac 30))
Evaluate
>
(letrec ((fib (lambda (n)
(if (< n 2)
1
(+ (fib (- n 1))
(fib (- n 2)))))))
(fib 30))
Evaluate
>
(+ 42 27)
Run JIT!
每次输入Scheme表达式时,都会将其解析为内部树状中间语言。然后,您可以通过按下“求值”按钮在该树上运行递归解释器。按几次,你会得到同样的结果。
解释器运行时,会记录它创建的任何闭包。附加到闭包的Func实例有一个C++函数指针槽,该槽最初为空。WebAssembly中的函数指针是间接函数表的索引;第一个插槽保持为空,因此调用空指针(值为0的指针)会导致错误。如果解释器到达闭包调用,并且闭包函数的JIT代码指针为空,它将解释闭包的主体。否则,它将调用函数指针。
如果您随后按下上面的“JIT”按钮,模块将组装一个新的WebAssembly模块,其中包含它在运行时看到的闭包的JIT代码。显然,这只是一个启发:你可能更渴望或更懒惰;这只是一个细节。
虽然对特定的JIT编译器不太感兴趣——重点是看JIT代码生成——但很高兴看到fibonacci示例看到了良好的加速;自己试试,如果可以的话,在不同的浏览器上试试。好东西!
不仅仅是网络
我想知道如何在非webby环境中实现这样的功能,而事实证明,wasmtime的Python接口正是如此。我写了一个小插页。py-harness可以做我们在网络上可以做的事情;只需以“python3 interp”的形式运行即可。py’,在“pip3安装wasmtime”之后:
$ python3 interp.py
...
Calling eval(0x11eb0) 5 times took 1.716s.
Calling jitModule()
jitModule result: <wasmtime._module.Module object at 0x7f2bef0821c0>
Instantiating and patching in JIT module
...
Calling eval(0x11eb0) 5 times took 1.161s.
有趣的是,wasmtime的代码(0.232s/调用)的性能似乎比SpiderMonkey(0.392s)和V8(0.729s)要好一些。
反思
这项工作只是概念证明,但它是朝着特定方向迈出的一步。作为Fastly之前工作的一部分,我们启用了SpiderMonkey JavaScript引擎在WebAssembly之上运行。当通过Wizer与预初始化相结合时,您将得到一个可以在微秒内启动的系统:例如,足够快,可以在每个HTTP请求上实例化一个新的无共享模块。
不过,WASI工作中的SpiderMonkey忽略了JIT编译,因为您知道,WebAssembly不支持JIT编译。JavaScript代码实际上是通过C++字节码解释器运行的。但正如我们刚刚发现的,实际上你可以编译字节码:只是在时间上,但在不同的时间尺度上。如果您使用SpiderMonkey解释器,为用户的JavaScript文件预先生成WebAssembly代码,然后通过Wizer将它们组合成一个冻干的WebAssemby模块,会怎么样?您可以享受快速启动的好处,同时也可以获得良好的基线性能。这里有许多工程方面的考虑,但作为Shopify赞助的工作的一部分,我们在这方面取得了良好的进展;另一封信中的细节。
我认为一种“离线JIT”对于Shopify和Fastly这样的部署环境有很大的价值,并且您不必将自己局限于“全面”优化:您仍然可以收集和合并类型反馈,并且您可以利用自适应优化,而不必在运行时实际运行JIT编译器。
但是,如果我们考虑更传统的“在线JIT”用例,很明显,依赖主机JIT功能虽然是一个好的MVP,但不是最佳的。首先,您希望能够自由地从生成的代码向现有代码发出直接调用,而不必间接调用或通过导入进行调用。我认为让一个语言运行时以WebAssembly模块的形式表达其生成的代码仍然是有意义的,尽管实际上您可能希望从WebAssemby本身中编译该代码(异步),而无需调用运行时。与我交谈过的大多数在JS引擎中进行WebAssembly实现的人都相信,JIT方案总有一天会出现,但我们不必等待它开始生成代码并利用它,这是很好的。
原文标题:just-in-time code generation within webassembly
原文链接:https://wingolog.org/archives/2022/08/18/just-in-time-code-generation-within-webassembly




