阅读 139

WebAssembly与即时编译器JIT

背景介绍

简介

WebAssemblywasm是一个实验性的低端编程语言,应用于浏览器内的客户端。WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及运行。WebAssembly将让开发者能运用自己熟悉的编程语言(最初以C/C++作为实现目标)编译,再藉虚拟机引擎在浏览器内运行。WebAssembly的开发团队分别来自Mozilla、Google、Microsoft、Apple,代表着四大网络浏览器Firefox、Chrome、Microsoft Edge、Safari。2017年11月,以上四个浏览器都开始实验性的支持WebAssembly。WebAssembly 于 2019 年 12 月 5 日成为万维网联盟(W3C)的推荐,与 HTML,CSS 和 JavaScript 一起,成为 Web 的第四种语言。(from wiki)

image.png

什么是WebAssembly

WebAssembly就是一段二进制代码,其本质是一个编译目标。

产生的契机

JavaScript 诞生于 1995 年。JavaScript是一种解释型语言、弱类型,它并不是为了快速运行而设计出来的,而且在最初十年里它的运行速度确实不够快。

优化阶段一: JIT

在 2008 年,这个被大家称为 性能战争 的时期开始了。很多浏览器添加了 即时编译器(just-in-time compilers),也被称作 JIT。当 JavaScript 代码运行时,JIT 可以识别代码运行的模式,并基于这些模式来加速代码的运行。

基本思想是,在 js 引擎中增加监视器,将可能会多次执行的代码段通过编译器编译存储,在下次调用时直接调用,并且对代码段进行优化。

由于 JIT 的出现,JavaScript 的性能迎来了一个拐点,JavaScript 的执行速度快了之前 10 倍。

随着性能的提高,JavaScript 开始被用于一些 令人难以预料 的领域,比如使用 Node.js 进行服务端编程。性能的提高使得 JavaScript 可以解决一类全新的问题。

JIT 的问题:

  1. 使执行的速度变快的同时,也带来了新的(主要是内存)性能开销

  1. JIT的优化机制会因为类型不确定而产生性能问题

优化阶段二:各种方案

针对于类型不确定产生的性能问题,产生了多种方案:

  1. 微软的 Typescript 通过为 JS 加入静态类型检查来改进 JS 松散的语法,提升代码健壮性;

  1. 火狐的 asm.js 是取 JS 的子集,JS 引擎针对 asm.js 做性能优化;

  1. 谷歌的 Dart/PNaCl 是为浏览器引入新的虚拟机去直接运行 Dart/C/C++ 程序以提升性能;

但是,每个方案其实都有各自的问题:

  1. Typescript 最后还是编译成 JS,性能没有太大的提升;

  1. asm.js 开发效率低;

  1. Dart/PNaCl 只能在 Chrome 里运行;

最终,大佬们达成一致,觉得 asm.js 这个方式有前途,于是标准化了一下,就是 WebAssembly。

WebAssembly 可能会带来另外另一个拐点。

工作原理

浏览器中的JavaScript如何运行

当你作为一个开发者,将 JavaScript 添加到页面的时候,你就有了一个目标 和 一个难题。

目标:告诉计算机应该做什么。

难题:你和计算机说的是不同的语言。

JavaScript 或 其他高级编程语言 其实都是人类语言。它们是为人类认知而设计,而不是机器。

JS引擎的工作就是将你的人类语言 翻译 为机器能够理解的机器语言。

在编程中,通常有两种 翻译 为机器语言的方式。我们可以使用 解释器(interpreter) 或者 编译器(compiler)。

在使用 解释器 时,这种 翻译 几乎是 一行一行 实时进行的。

编译器 使用另一种方式,它会提前 翻译 好 并且记录下来,而不是动态进行 翻译 。

解释器 的 优缺点

解释器 可以迅速开始工作 并 运行代码。不必完成整个编译阶段,就能开始运行代码了。可以一行一行地 翻译 并运行。

解释器似乎很自然地适用于 JavaScript。因为对于 Web 开发者来说,能够快速运行自己的代码是很重要的。这确实是浏览器最初使用 JavaScript 解释器的原因。

但是,当不止一次地运行相同代码时,解释器的缺点就出现了 。例如,在一个循环中,它将不得不 一次又一次的做出同样的 翻译

编译器 的 优缺点

编译器 权衡利弊的方式 和 解释器是相反的。

因为必须先进行编译阶段,所以它启动需要花更多的时间。但由于不需要在循环中重复进行 翻译 ,在循环中的代码会运行的更快。

另外一个不同之处是,编译器有更多的时间对代码进行分析和修改,以便代码能够运行的更快。这种修改被称为 优化(optimization)。

解释器 作用在 运行时(runtime),所以在 翻译 时没有足够的时间来计算这些 优化

即时编译器JIT(Just-in-time compiler)

没有 JIT 的浏览器执行 JavaScript 的过程

为了避免解释器的低效性,即每次经过循环都需要重新 翻译 代码,浏览器开始混合加入编译器。

不同的浏览器以略微不同的方式实现这一点,但基本思想是一致的。它们将 监视器 作为一个新部件 添加到浏览器引擎中。监视器 在代码开始运行时开启监控,并记录代码 运行的次数 和 使用到的类型

一般来说,JavaSciript 引擎都有一个包含解释器优化编译器的处理流程。其中,解释器可以快速生成未优化的字节码,而优化编译器会耗费更长的时间,但最终可生成高度优化的机器码。

????????

先了解几个概念:

IgnitionV8引擎中的解释器(interpreter)
TurbofanV8引擎中的优化编译器(optimizing compiler)
BaselineSpiderMonkey引擎中的基线编译器
IonMonkeySpiderMonkey引擎中的优化编译器
桩代码(Stub)用来替换一部分功能的程序段
warm用来标记代码执行频次,如果同一行代码运行过几次,那么这段代码就被称为 warm
hot用来标记代码执行频次,如果同一行代码运行了非常多次,那么这段代码就被称为 hot
优化为了使代码运行更快,优化编译器做出一些假设,生成优化代码
去优化代码回退到解释器或者基线编译器编译的版本进行执行

V8(Chrome和Node使用的JS引擎)

SpiderMonkey(Firefox使用的JS引擎)

我们以SpiderMonkey为例详细说明一下:

最初 ,监视器 只通过 解释器 运行所有代码。

基线 编译器(Baseline compiler)

当一个函数开始变 warm,JIT 就将它发送出去进行编译。之后 JIT 会将编译结果存储下来。

函数的每一行代码都会分别被编译成一段 “桩代码”,桩代码 根据 行号变量类型 进行索引。如果 监视器 发现代码执行中出现了 执行过的相同代码(且使用的变量类型相同),那么就会取出 已编译 的版本。

这有助于加速代码运行。但是就像我在上文提到的,编译器能做到的不仅仅是这些。它还能花些时间来计算出代码最有效率的执行方式,然后进行优化。

基线编译器 会执行上面提到的优化。但优化花费时间不应该太长,这会导致代码执行被阻塞。

可是,如果代码真的非常 hot ,即代码运行了非常多次,那么花费额外的时间去做些优化就是值得的。

优化编译器(Optimizing compiler)

当一部分代码非常 hot 的时候,监视器 就会将它发送到 优化编译器。这将创建并保存另一个版本的函数代码,这份代码会比原版的运行更快。

为了使代码运行更快,优化编译器不得不做出一些假设

例如,如果可以假设 所有的对象都由一个 特定形式构造函数 创建 — 即对象总是具有相同且添加顺序相同的属性名,那么就可以基于这个假设,走一些捷径。

通过监视器观测代码执行收集到的信息,优化编译器就可以做出判断。如果某件事在之前的循环都是发生的,那么假定这件事还会继续发生。

但是,当然,对于 JavaScript,这是得不到保证的。就算有 99 个对象都有相同的结构,第 100 个也有可能缺失一个属性。

所以,需要 在编译后的代码运行之前,对其进行检查,确认之前的假设是否还有效。如果通过检查,那么就执行代码。但如果没有通过,JIT 会认为 自己之前做出了错误的假设,并清除优化版本的代码(去优化)。

通常,优化编译器会加速代码运行,但是有时也会导致意外的性能问题。如果代码持续不断的进行 优化 ,然后 去优化,那么最终 这会比直接执行 基线编译器编译的代码 还要慢。

大多数浏览器会增加一些限制,用来打断可能出现的 优化/去优化 循环周期。如果 JIT 已经做了 比方说 10 次优化,并且不断的将优化代码清除,那么它就会放弃再进行优化了。

这就是 JIT。它 监控代码运行情况 并 将 hot 代码进行优化,以此让 JavaScript 运行得更快。是它的出现让大多数 JavaScript 应用程序的性能提高了许多倍。

尽管有了这些改进,JavaScript 的性能还是不稳定的。为了让性能整体更快,JIT 运行时会有许多开销,包括:

  1. 优化 和 去优化

  1. 监视器 记录监视信息 所消耗的内存

  1. 去优化 的恢复信息 消耗的内存

  1. 存储 基线 编译器 和 优化编译器 不同的函数编译版本 所消耗的内存

WebAssembly

WebAssembly 所处的位置

早期的JS:

当前的JS:

WebAssembly 与典型 web 应用对比

展望

2019年12月份,正式加入w3c规范,作为和html,css,js并行的第四种语言

目前看来 WebAssembly 主要的两个点是:

  1. 适合用于大量密集计算,且无需频繁与 JS 和 DOM 交互的场景,比如音视频处理,dom diff

  1. 可以实现代码库的复用


作者:过境千帆
链接:https://juejin.cn/post/7054465321230401566

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐