WebAssembly 如何演进成为“浏览器第二编程语言”?


WebAssembly 如何演进成为“浏览器第二编程语言”?

文章插图
WebAssembly 无疑是近年来让人最为兴奋的新技术之一 , 它虽始于浏览器但已经开始不断地被各个语言及平台所集成 。在实际的工业化落地中 , 区块链、边缘计算、游戏及图像视频等多个领域都依靠 WebAssembly 创造了让人称赞的产品 。WebAssembly 技术本身具有非常多优点 , 其中最为被人所熟知的三点有:
  • 二进制格式
  • Low-Level 的编译目标
  • 接近 Native 的执行效率
那么 WebAssembly 是从何演变而来 , 它为什么具有这些优点与特性 , 又是如何被标准化的 , 更重要的是作为普通开发者 , 我们应如何更好地入手 WebAssembly 开发及实践呢?本专题将围绕 WebAssembly 及 Emscripten 工具链 , 通过一系列文章依次介绍 WebAssembly 的演变历程、工具链使用、实践案例、最新应用场景及使用技巧 , 帮助普通开发者正确理解 WebAssembly 的使用场景 , 并能够顺利使用 Emscripten 工具链完成自己的 WebAssembly 相关项目 。
本文作为专题的第一篇文章 , 将会较为详细地介绍 WebAssembly 的演变历程 , 使读者深入理解 WebAssembly 这门技术的使用场景 , 从而更好地学习和使用 WebAssembly 技术 。
JAVAScript 的弊端JavaScript 毫无疑问是技术领域的佼佼者 。自 Brendan Eich 于 1995 年花费 10 天时间为 Netscape 开发出 JavaScript 为始 , 到现在已经走过了 20 多个年头 。随着技术的蓬勃发展 , 不管是 NPM 与 GitHub 上丰富的 JavaScript 库与框架 , 还是 React Native 、Node.js、Electron、QuickJS 等领域技术的出现 , 无一不彰显着 JavaScript 生态的繁荣 , JavaScript 这门语言也变得越来越流行和重要 。
但与此同时 , 随着各类应用功能的复杂化 , 受限于 JavaScript 语言本身动态类型和解释执行的设计 , 其性能问题也逐渐凸现 。我们急需新技术帮助我们解决 JavaScript 的性能问题 。在 2008 年底 , google、Apple、Mozilla 为 JavaScript 引入了 JIT(Just-In-Time)引擎 , 试图解决 JavaScript 的性能问题 , 并取得了非常好的效果 。其中的佼佼者非 Google 的 V8 莫属 , 其大举提升了 JavaScript 的性能 , 并拉开了 JavaScript 引擎竞速的序幕 。
那 JIT(Just-In-Time)引擎是如何提升 JavaScript 性能的呢?
我们知道 , 由于 JavaScript 是解释型语言 , 因此 JavaScript 引擎需要逐行将 JavaScript 代码翻译为可执行的代码 。可执行代码有多种形式 , 其中较为常见的是基于 AST 的直接执行以及 ByteCode 的执行方式 。显而易见 , 这些做法相比于直接运行机器码而言都并不高效 , 如果我们能根据代码的执行频次将部分代码实时编译为机器码 , 就能获得更大的性能提升 。这就是 JIT(Just-In-Time)的基本思路 。
在实际生产中 , JIT(Just-In-Time)引擎一般会引入多层次的决策来优化代码:
  • warm 阶段(解释执行的代码被执行多次): 将解释执行的代码发送给 JIT(Just-In-Time)引擎 , 并创建出编译为机器码的执行代码 , 但此处并不进行替换;
  • hot 阶段(解释执行的代码被执行得十分频繁): 解释执行代码被替换为 warm 阶段的机器码执行代码;
  • very hot 阶段:将解释执行的代码发送给优化编译器(Optimising Compiler) , 创建和编译出更高效的机器码的执行代码并进行替换;
假设我们的 JavaScript 代码中有部分代码被执行了多次 , 此时这部分代码会被标记为 warm , 同时被送往 JIT(Just-In-Time)引擎进行优化 。JIT(Just-In-Time)引擎此时会针对这些代码逐行进行机器码编译 , 然后存储在一张表的单元中(实际上表单元仅指向了被编译的机器码) 。当解释执行的代码被执行得非常频繁时会进入 hot 阶段 , JIT(Just-In-Time)引擎会将解释执行的代码直接替换为编译的机器码版本 。
需要注意的是 , 表单元的引用依据实际上会依赖于行号以及参数类型 , 假设我们有如下的代码:
function doSomething(value){// performing some operations}const arr = [0, "String"];for (let i = 0; i < arr.length; i++) {doSomething(arr[i])}由于数组 arr 中存在两种数据类型(Number/String) , 当我们多次执行相关代码时 , doSomething函数会被 JIT(Just-In-Time)引擎创建并编译出两个不同类型的机器码执行代码版本 , 并且使用不同的表单元引用 。当然 , 由于机器码执行代码的创建和编译存在代价 , 因此不同的 JIT(Just-In-Time)引擎会有不同的优化策略 。


推荐阅读