(转)WASM(WebAssember)快速了解第二篇——快速了解JIT

这是有关WebAssembly的系列文章的第二部分,如果您还没有阅读其他文章,我们建议从头开始

JavaScript的启动速度很慢,但后来有了所谓的JIT,它变得更快。但是,JIT如何工作?

浏览器如何运行JavaScript

当您作为开发人员向页面添加JavaScript时,您就有目标和问题。

目标:您想告诉计算机该怎么做。

问题:您和计算机使用不同的语言。

您说人类语言,而计算机说机器语言。即使您不将JavaScript或其他高级编程语言视为人类语言,也是如此。它们是为人类认知而不是机器认知而设计的。

因此,JavaScript引擎的工作是采用您的人工语言并将其变成机器可以理解的东西。

我认为这就像电影《到来》,其中有人和外星人试图互相交谈。

在那部电影中,人类和外星人不只是逐字翻译。两组对世界的看法不同。人和机器也是如此(我将在下一篇文章中对此进行更多说明)。

那么翻译如何发生?

在编程中,通常有两种翻译成机器语言的方法。您可以使用解释器或编译器。

有了翻译员,这种翻译几乎是逐行进行的。

 另一方面,编译器不会即时进行翻译。它可以提前创建该翻译并将其记录下来。

这些处理翻译的方式各有利弊。

解释器的利弊

解释器可以快速启动并运行。在开始运行代码之前,无需完成整个编译步骤。您只需开始翻译第一行并运行它。

因此,解释器似乎很适合JavaScript之类的东西。对于Web开发人员而言,能够快速开始并运行其代码非常重要。

这就是为什么浏览器最初使用JavaScript解释器的原因。

但是,当您多次运行相同的代码时,就会使用解释器。例如,如果您处于循环中。然后,您必须一遍又一遍地进行相同的翻译。 

编译器的优缺点

编译器具有相反的权衡。

启动需要花费更多时间,因为它必须在开始时执行该编译步骤。但是随后循环中的代码运行得更快,因为它不需要为每次通过该循环重复翻译。

另一个区别是,编译器有更多时间查看代码并对其进行编辑,以使其运行更快。这些编辑称为优化。

解释器在运行时进行工作,因此在翻译阶段可以花很多时间来找出这些优化。

即时编译器:两全其美

作为摆脱解释器效率低下的一种方式(浏览器每次循环时都必须不断重新翻译代码),浏览器开始将编译器混入其中。

不同的浏览器以略有不同的方式执行此操作,但是基本思想是相同的。他们向JavaScript引擎添加了一个新部件,称为监视器(又称为探查器)。该监视器在代码运行时对其进行监视,并记录其运行了多少次以及使用了哪种类型。

首先,监视器只是通过解释器运行所有内容。

如果同一行代码运行了几次,则该段代码称为热代码。如果运行很多,则称为高温。

基准编译器

当功能开始变热时,JIT会将其发送出去进行编译。然后它将存储该编译。

函数的每一行都被编译为一个“存根”。存根由行号和变量类型索引(稍后将解释为什么这很重要)。如果监视器发现执行再次使用相同的变量类型命中相同的代码,则它将仅提取其编译版本。

这有助于加快速度。但是就像我说的,还有更多的编译器可以做。找出解决方案的最有效方法可能需要一些时间。

基准编译器将进行其中的一些优化(我在下面给出一个示例)。但是,它不想花费太多时间,因为它不想使执行时间太长。

但是,如果代码真的很热(如果正在运行很多次),那么值得花费额外的时间进行更多的优化。

优化编译器

当一部分代码非常热时,监视器会将其发送给优化的编译器。这将创建该功能的另一个甚至更快的版本,该版本也将被存储。

为了使代码的版本更快,优化的编译器必须做出一些假设。

例如,如果可以假设由特定构造函数创建的所有对象都具有相同的形状(即它们始终具有相同的属性名称,并且这些属性以相同的顺序添加),则它可以基于在那。

优化编译器通过监视代码执行情况来使用监视器收集的信息来做出这些判断。如果对于先前通过循环的所有遍历都为真,则假定它将继续为真。

但是,当然,对于JavaScript,永远不会有任何保证。您可能拥有全部具有相同形状的99个对象,但是第100个对象可能缺少属性。

因此,编译后的代码需要在运行之前进行检查,以查看这些假设是否有效。如果它们是,则编译的代码将运行。但是,如果不是这样,JIT会假设自己做出了错误的假设,并浪费了优化后的代码。

然后执行返回到解释器或基准编译版本。此过程称为反优化(或应急)。

通常,优化编译器可使代码更快,但有时它们可​​能会导致意外的性能问题。如果您的代码不断进行优化,然后再进行优化,那么最终结果将比仅执行基准编译版本慢。

大多数浏览器都增加了限制,以在发生这些优化/反优化周期时突围而出。如果JIT进行了10次以上的优化尝试,而又不得不将其扔掉,它将停止尝试。

优化示例:类型专门化

有很多不同类型的优化,但是我想看看一种类型的优化,以便您可以感觉到优化是如何发生的。优化编译器的最大胜利之一就是所谓的类型专门化。

JavaScript使用的动态类型系统在运行时需要一些额外的工作。例如,考虑以下代码:

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

+=循环中的步骤似乎很简单。看起来您可以一步计算出来,但是由于动态键入,它需要的步骤比您预期的要多。

假设这arr是一个100个整数的数组。代码预热后,基线编译器将为函数中的每个操作创建一个存根。因此,将有一个存根sum += arr[i],它将+=作为整数加法处理操作。

但是,sumarr[i]不能保证为整数。由于类型在JavaScript中是动态的,因此在循环的后续迭代中arr[i]可能会有一个字符串。整数加法和字符串串联是两个非常不同的操作,因此它们将编译为非常不同的机器代码。

JIT处理此问题的方法是编译多个基准存根。如果一段代码是单态的(即始终以相同的类型调用),它将得到一个存根。如果它是多态的(从一种代码传递到另一种代码使用不同的类型调用),那么它将为通过该操作的每种类型的组合获取一个存根。

这意味着JIT在选择存根之前必须先问很多问题。

由于基线编译器中每行代码都有其自己的存根集,因此,每次执行该行代码时,JIT都需要继续检查类型。因此,对于循环中的每次迭代,都必须提出相同的问题。

如果JIT不需要重复这些检查,则代码的执行速度将大大提高。这就是优化编译器要做的事情之一。

在优化编译器中,整个函数将一起编译。移动类型检查,使它们在循环之前发生。

一些JIT对此进行了进一步优化。例如,在Firefox中,对于仅包含整数的数组有一个特殊的分类。如果arr是这些数组之一,则JIT不需要检查是否arr[i]为整数。这意味着JIT可以在进入循环之前执行所有类型检查。

结论

简而言之,这就是JIT。通过监视正在运行的代码并发送要优化的热代码路径,它可以使JavaScript运行更快。这导致大多数JavaScript应用程序的性能得到了许多方面的改进。

即使进行了这些改进,JavaScript的性能仍然是不可预测的。为了使事情更快,JIT在运行时增加了一些开销,包括:

  • 优化和反优化
  • 内存,用于监视器的簿记和恢复信息,以了解何时发生救助
  • 用于存储功能的基准和优化版本的内存

这里还有改进的余地:可以消除开销,使性能更可预测。这就是WebAssembly要做的事情之一。

接下来的文章中,我将更多地解释装配和编译器如何使用它。

转自:https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/

原文地址:https://www.cnblogs.com/wangzxblog/p/11674104.html