使JavaScript在WebasseMbly上快速运行

2021-06-03 23:39:39

浏览器中的JavaScript在二十年前的速度比它更快地运行。这发生了,因为浏览器供应商花了那时正在研究强化性能优化。

今天,我们正在开始优化完全不同环境的JavaScript性能,不同的规则适用。这是可能因为webassembly而可能的。

我们应该清除这里 - 如果您在浏览器中运行JavaScript,它仍然是简单地部署JS的最有意义。浏览器中的JS引擎高度调整以运行已运送到它们的JS。

但是,如果您在无服务器函数中运行JavaScript,该怎么办?或者如果要在不允许常规立即编译的环境中运行JavaScript,如iOS或游戏控制台?

对于那些用例,您将要注意这一新的JS优化浪潮。而这项工作也可以作为其他运行时的模型 - 例如Python,Ruby和Lua - 希望在这些环境中快速运行。

但在我们探索如何使这种方法快速运行之前,我们需要查看它在基本级别的工作原理。

每当您运行JavaScript时,JS源代码都需要以一种方式执行作为机器代码。这是由JS引擎使用的各种技术(如解释器和JIT编译器)完成。 (有关更多详细信息,请参阅即时(JIT)编译器中的崩溃课程。)

但是如果您的目标平台没有JS引擎,该怎么办?然后,您需要与您的代码一起部署JS引擎。

为此,我们将JS Engine作为WebasseMbly模块部署,这使其在不同类型的机器架构上便携。与Wisi一起,我们也可以使其在不同的操作系统上便携。

这意味着整个JS环境捆绑在此WebasseMbly实例中。部署后,所有您需要做的就是在JS代码中为源,它将运行该代码。

JS Engine而不是直接在机器的内存上工作,而是将所有内容从字节码放入字节码在WASM模块的线性存储器中操作的GCED对象。

对于我们的JS引擎,我们与Spidermonkey一起使用,该蜘蛛侠在Firefox中使用。它是工业强度JavaScript VM之一,在浏览器中进行战斗。当您运行不受信任的代码或处理不受信任的输入的代码时,这种Battleting和安全投资很重要。

Spidermonkey还使用称为精确堆栈扫描的技术,这对于我在下面解释的一些优化很重要。它还具有一个高度平易熟的码级,这很重要,因为来自3个不同组织的BA成员 - 速度,Mozilla和Igalia - 正在合作。

到目前为止,关于我描述的方法没有什么革命性的。多年来,人们已经用网络装配方式运行了JS。

问题是它很慢。 WebAsseMbly不允许您动态生成新的机器代码并从纯WASM代码中运行它。这意味着您无法使用JIT。您只能使用翻译。

由于JITS是浏览器使JS快速运行的方式(并且由于您不能在网上装配模块内编译),因此似乎对此执行此操作。

让我们看几种使用案例,这种方法的快速版本可能非常有用。

由于安全问题,您无法使用JIT的一些地方 - 例如,未经特权的iOS应用程序和一些智能电视和游戏控制台。

在这些平台上,您必须使用翻译。但是,您在这些平台上运行的应用程序是长期运行的,它们需要大量代码......这些代码恰好是历史上你不想使用翻译的条件,因为它减慢了多少执行。

如果我们能够快速使我们的方法能够快速,那么这些开发人员可以在不符合绩效的平台上使用JavaScript而不达到巨大的绩效。

还有JITS不是问题的其他地方,但在启动时间是问题的地方,就像在无服务器函数中一样。这是您可能听说过的冷启动延迟问题。

即使您使用最多配对的JS环境 - 一个刚刚启动裸js引擎的隔离,您即可至少查阅〜5毫秒的启动延迟。这甚至没有包括初始化应用程序所需的时间。

有一些方法可以隐藏此启动延迟的来电延迟。但它越来越难以隐藏它,因为在网络层中在网络层中优化了连接时,在诸如Quic的提案中。当您正在进行多个无服务器功能时,它也更加困难。

使用这些技术隐藏延迟的平台也经常在请求之间重用实例。在某些情况下,这意味着可以在不同的请求之间观察到全局状态,这是一种安全危险。

由于这种冷启动问题,开发人员往往不遵循最佳实践。他们将很多功能填充到一个无服务器部署中。这导致另一个安全问题 - 更大的爆炸半径。如果利用该无刀部署的一部分,则攻击者可以访问该部署中的所有内容。

但如果我们可以在这些上下文中获得足够低的JS启动时间,那么我们不需要用任何技巧隐藏启动时间。我们可以在微秒中启动一个实例。

有了这个,我们可以在每个请求中提供一个新的实例,这意味着请求之间没有伴随着伴随的状态。

因此,由于该实例是如此轻量化,开发人员可以随意将其代码分解为细粒块,使爆炸半径带到任何单件代码的最小代码。

这种方法还有另一个安全效益。除了轻量化并使其有可能具有更精细的隔离之外,安全界发动机提供更加可靠。

由于用于创建隔离的JS引擎是大的CodeBases,因此包含大量的低级代码进行超复杂的优化,因此允许攻击者转义VM并访问VM正在运行的系统的错误很容易。 。这就是为什么浏览器像Chrome和Firefox这样的浏览器转到很长的长度,以确保网站在完全分隔的流程中运行。

相比之下,WASM引擎需要更少的代码,因此它们更容易审核,其中许多人都以锈迹,内存安全的语言编写。可以验证从网主模块生成的本机二进制文件的存储器隔离。

通过在WASM引擎内运行JS发动机,我们将这种外部,更安全的沙箱边界作为另一条防线。

因此,对于这些用例,可以快速使JS在WASM上进行大笔好处。但我们怎样才能做到这一点?

为了回答这个问题,我们需要了解JS引擎花费时间的位置。

我们可以分解JS引擎的工作大致两部分:初始化和运行时。

我认为JS发动机作为承包商。保留此承包商以完成运行JS代码并获得结果。

在此承包商实际上可以开始运行项目之前,它需要做一些初步的工作。初始化阶段包括在执行开始时只需要发生一次的所有内容。

对于任何项目,承包商需要查看客户希望它做的工作,然后设置完成该任务所需的资源。

例如,承包商通过项目简报和其他支持文件读取,并将它们转化为它可以使用的东西,例如,使用存储和组织的所有文档设置项目管理系统。

在JS引擎的情况下,此工作看起来更像通过源代码的顶级读取,并将函数解析为字节码,分配已声明的变量的内存,以及设置它们已定义的值。

在某些上下文中,如无要的,在每个应用程序初始化之前发生初始化的另一部分是初始化。

这是引擎初始化。 JS引擎本身需要首先启动,并且需要将内置功能添加到环境中。

我想到这一点就像在开始工作之前,让办公室本身做出像装配宜家椅子和桌子一样的事情。 这可能需要相当长的时间,并且是可以对无服务器使用情况进行冷启动这样一个问题的一部分。 完成初始化阶段后,JS引擎可以启动运行代码的工作。 这部分工作的速度称为吞吐量,此吞吐量受大量不同变量的影响。 例如: 代码是否运行足够长,可以从JS引擎的优化编译器中受益 我们开始用一个名为Wizer的工具快速进行初始化。 我将解释如何,但对于那些不耐烦的人来说,这里的速度是我们在运行一个非常简单的JS应用程序时看到的速度。 使用Wizer运行此小应用程序时,它只需要.36毫秒(或360微秒)。 这比我们对JS隔离方法的期望快13倍。

我们使用称为快照的东西来获得快速启动。 Nick Fitzgerald在他的WebasseMbly Summit谈论Wizer中更详细地解释了这一切。

那么这是如何工作的?在部署代码之前,作为构建步骤的一部分,我们使用JS引擎运行JS代码到初始化结束。

此时,JS引擎已经解析了所有JS并将其转换为字节码,JS引擎模块存储在线性存储器中。该引擎在此阶段也有很多内存分配和初始化。

因为这种线性存储器是如此自包含,所以一旦所有的值都填写,我们就可以拍摄存储器并将其作为数据部分附加到WASM模块。

当实例化JS引擎模块时,它可以访问数据部分中的所有数据。每当发动机需要一点内存时,它可以复制它需要它所需要的线性存储器的部分(或相当的内存页面)。有了这个,JS引擎在启动时不必执行任何设置。所有预先初始化的值都准备好并等待它。

目前,我们将此数据部分附加到与JS引擎相同的模块。但是,在未来,一旦模块链接到位,我们将能够将数据部分作为单独的模块运送,允许JS引擎模块被许多不同的JS应用程序重用。

JS引擎模块仅包含引擎的代码。这意味着一旦编译,可以在许多不同的实例之间有效地缓存并重复使用该代码。

另一方面,特定于应用程序的模块不包含WASM代码。它仅包含线性内存,又包含JS字节码,以及初始化的JS引擎状态的其余部分。这使得它真的很容易移动这种内存并在需要去的地方发送它。

这就像JS发动机承包商甚至根本不需要办公室。它只是获得了一个运往它的旅行案例。旅行案例具有整个办公室,其中一切都在其中,所有设置和准备好JS引擎即可开始工作。

最酷的事情是,它不是依赖于依赖的 - 它只是使用webassembly的现有属性。因此,您也可以使用与Python,Ruby,Lua或其他运行时相同的技术。

因此,通过这种方法,我们可以获得超快速启动时间。但吞吐量怎么样?

对于某些用例,吞吐量实际上不会太糟糕。如果您有一个非常短的跑步爪,无论如何都不会经过JIT - 它将整个时间留在口译员中。因此,在这种情况下,吞吐量与浏览器中的吞吐量大致相同,并且在传统的JS引擎完成初始化之前完成。

但对于更长的运行JS,在JIT开始踢出之前,它不会需要那么长时间。一旦发生这种情况,吞吐量差异开始变得显而易见。

正如我上面所说,目前无法在Pure WebAssAssembly中提示编译代码。但事实证明,我们可以应用JITS的一些思想,以提前的编译模型。

JITS使用的一种优化技术是内联缓存。通过内联缓存,JIT将创建一个包含快速机器代码路径的存根链接列表,所有方法都在过去的一系列JS字节码。 (有关更多详细信息,请参阅即时(JIT)编译器中的崩溃课程。)

您需要列表的原因是因为JS中的动态类型。每次代码行使用不同类型时,您都需要生成一个新存根并将其添加到列表中。但是,如果您之前遇到此类型,那么您只需使用已为此生成的存根。

因为内联的缓存(IC)通常用于JITS,所以人们认为它们非常动态并且对每个程序特定。但事实证明,它们也可以在AOT上下文中应用。

甚至在我们看到JS代码之前,我们已经知道了很多IC存根,我们将需要生成。这是因为JS中有一些模式,可以使用很多。

一个很好的例子是访问对象上的属性。在JS代码中发生了很多,并且可以使用IC存根来加速它。对于具有某个“形状”或“隐藏类”的对象(即,以相同方式布置的属性),当您从这些对象获取特定属性时,该属性将始终处于相同的偏移量。

传统上,JIT中的这种IC存根将硬核代码两个值:指向该属性的形状和偏移量。这需要我们没有措施的信息。但我们可以做的是参数化IC存根。我们可以将形状和属性偏移作为传递给存根的变量。

这样,我们可以创建从内存加载值的单个存根,然后在任何地方使用同一个存根代码。无论JS代码实际上,我们都可以将这些常见模式的所有存根烘烤到AOT编译的模块中。即使在浏览器设置中,该IC共享也有益,因为它允许JS引擎生成更少的机器代码,从而提高启动时间和指令缓存局部性。

但对于我们的用例,它尤为重要。这意味着我们可以将这些常见模式的所有存根烘烤到AOT编译的模块中,而不管JS代码实际上是什么。

我们发现,只有几千字节的IC存根,我们可以涵盖所有JS代码的绝大部分。例如,对于2 kB的IC存根,我们可以在Google Octane基准中覆盖95%的JS。并且从初步测试中,此百分比似乎也适用于一般的Web浏览。

因此,使用这种优化,我们应该能够达到与早期JITS相符的吞吐量。一旦我们完成了那项工作,我们将增加更细粒度的优化和波兰语性能,就像浏览器的JS引擎团队用他们的早期JITS一样。

这就是我们可以提前做的事情,而不知道程序所做的是什么以及流过它的类型。

但是,如果我们访问了JIT的同类貌相信息,那么然后我们可以完全优化代码。

这里有一个问题,虽然开发人员经常有很难分析自己的代码。很难提出代表性的样品工作负载。所以我们不确定我们是否可以获得良好的分析数据。

如果我们能够弄清楚将良好的工具放在适当的貌相中,那么我们实际上可以使JS几乎可以像今天的JITS一样快速地运行(没有热身时间!)

我们对这种新方法感到兴奋,并期待看到我们可以推动多远。我们也很高兴看到其他动态类型的语言以这种方式来到网页装配。

所以这里有几种方法来开始今天,如果您有任何疑问,可以在Zulip中提问。

要在您自己的平台中运行JS,您需要嵌入支持WASI的WebasseMbly引擎。我们正在使用WASMTIME。

然后你需要你的JS引擎。作为这项工作的一部分,我们已经为Mozilla的构建系统添加了对编译Spidermoonkey到Wasi的完全支持。和Mozilla即将为SpiderMoNkey添加Wasi构建到用于构建和测试Firefox的相同CI设置。这使得SPIDermonkey的生产质量目标并确保了WASI Build继续随时间工作。这意味着您可以以相同的方式使用SpiderMoonkey。

最后,您需要用户携带预初始化的JS。为了帮助解决此问题,我们还开辟了Sourced Wizer,您可以集成到BuildTool中,该构建工生成特定于应用程序的WebasseMbly模块,该模块填写JS引擎模块的预初始化内存。

如果您是Python,Ruby,Lua等语言语言社区的一部分,您也可以为您的语言构建一个版本。

首先,您需要将运行时编译为webassembly,使用wasi for系统调用,正如我们使用spidermonkey的那样。然后,要获取快速启动时间与快照,可以将Wizer集成到BuildTool中以生成内存快照,如上所述。