搜索
当前位置: 7303刘伯温开奖6374 > 调用 >

【第1451期】在 JavaScript 和 WebAssembly 之间调用执行速度终

gecimao 发表于 2019-06-06 09:48 | 查看: | 回复:

  WebAssembly可能离的有点远,做个大概的了解。今日早读文章由@前端开发-晓翻译分享。

  在 Mozilla,我们希望 WebAssembly 的执行速度能够达到它应该达到的速度。

  这与它的设计(有很大的吞吐量)有关。然后我们用一个流基线编译器(streaming baseline compiler)改进加载时间。使用这项技术,我们编译代码的速度比从网络上加载到本地还要快。

  我们项目重点之一 - 很容易地将 JS 和 WebAssembly 结合起来。但是一直以来两种语言之间调用函数并不是很快。实际上,两种语言之间的函数调用速度是出了名的慢,我之前的WebAssembly 系列中讲到过。

  因此目前这些调用在 Firefox 很快了。但是,一如既往,我并不仅仅是告诉你这些调用很快。我想要解释我们是如何做到的。因此,让我们看下是如何在 Firefox 中改进每一种调用(嗨还有改进的程度)。

  但是,首先我们看下引擎是如何处理这些调用的。(如何你已经知道引擎是如何处理函数调用的,你可以跳过这一部分)

  像我之前的第一个 WebAssembly 系列中解释的那样,使用像 Java 编程语言和计算机理解的语言不一样。为了执行代码,我们下载的 .js 文件需要转换为机器能够理解的机器码。

  每个浏览器都有内置的翻译器。翻译器有时候被称为 Java 引擎或者是 JS 运行时。然而,这些引擎现在也能处理 WebAssembly ,这样的术语可能混淆。在这篇文章中,我们称它为引擎。

  当浏览器遇到一些 JavaScipt 代码,它会启动引擎运行代码。引擎会用自己的方式,到达需要调用的函数直到文件结束。

  假设我们想玩康威的生活游戏。引擎为我们渲染画面,但是事实证明并没有那么简单…

  因此引擎检查下一个函数,但是下一个函数将会通过调用更多的函数发送给引擎更多的人物。

  如果引擎之前的步骤正确 - 如果给了正确的函数正确的参数,能够用它自己的方式回到起始函数 - 它需要追踪一些信息。

  它通过使用一种被称为栈帧(调用帧)的方式实现的。每个函数对应一张表单,其中有函数的参数,还有返回值的地址,并且包含这个函数创建的本地变量。

  它通过将这些带有表单的纸张放在一个栈里面来追踪它们。栈顶的纸张是当前函数正在处理的。当处理完一个函数,丢掉函数对应的表单。因为是一个栈,有一张在栈最下面的一张纸。我们需要返回值到这个地方。

  引擎建立这个堆栈。随着函数调用,帧被添加到栈中。随着函数返回,帧被从栈中移除。一直保持这种变化,直到一切帧从栈中弹出。

  这就是函数调用基本的工作方式。目前,让我们看下是为什么在 Java 和 WebAssembly 之间的函数调用如此之慢,并且谈下我们是如何在 Firefox 中让它变的很快。

  当引擎经过你的代码,它不得不处理有两种语言编写的函数 - 即使你的函数都是用 Java 编写的。

  那些运行在解释器中的代码 - 被转换为字节码。这是一种比 Java 跟接近机器码的一种源码,但并不是机器码。这运行的相当快,但是并没有达到理想的状态。

  其他的一些函数 - 那些被频繁调用的 - 被JIT(just-in-time)编译器直接转换为机器码。这些被转换为机器码的代码不会在解释器中运行。

  引擎需要能够在这些大洲之间来回穿梭。但是当它在这些大洲之间来回跳跃的时候,需要一些信息,比如:另一个大洲的相对位置(它需要跳回来)。引擎也会按需要分离这些帧(引擎也需要在帧与帧之间来回穿梭)。

  为了组织工作,引擎会创建一个文件夹,然后把需要的信息放在旅行的时候的口袋里 - 例如:它从哪里进入大陆。

  它将会使用口袋去存储栈帧,口袋会随着引擎在大洲上产生越来越多的栈帧扩大。

  每当它切换到一个新的大洲,引擎会新建一个文件夹。新建一个文件夹唯一的问题是,它必须通过 C++。通过C++ 增加大量成本。

  这是一个在我的第一个 WebAssembly 系列中谈到的一个蹦床运动。

  在我们的大洲游戏比喻中,在每趟旅行两个大洲之间的蹦床点都有一个强制性的短暂的停留。

  当我们第一次添加 WebAssembly 的支持时,我们有不同类型的文件夹。因此尽管经过 JIT 的代码和 WebAssembly 代码都被编译为机器语言,但我们把它们看作不同的语言。我们将它们看作是在分割的大洲上面。

  我们通过为 JIT 过的代码和 WebAssembly 的代码归纳为一个文件夹来修复这个问题。就像是我们将两个大洲组合在了一起,让你在这两个大洲之间切换不需要蹦床。

  尽管经过 JIT 的 Java 代码,和 WebAssembly 说同样的语言,但它们仍然有不同的习俗。

  因为 Java 中变量没有明确的类型,类型需要在运行时确定。引擎通过为值添加一个标志,来追踪值的类型。

  就好像 Java 在值周围放了一个箱子,箱子包含那个代表值类型的标志。例如,末尾的 0 代表整型。

  为了计算两个整数的和,系统需要移除箱子。比如,为变量 a 移除箱子,然后为变量 b 移除箱子。

  这将你期望的1个操作变成了4个操作.. 即使在某些情况下,你并不需要“装箱”操作(比如静态类型的语言),不想让这成为负担。

  旁注:Java JITS 在很多情况下可以避免这种 “封箱解箱” 的操作,但在一般情况下,比如函数调用,需要回到”封箱“操作。

  这就是为什么 WebAssembly 期望”解箱“ 过的参数,和不”封箱“函数返回值。因为 WebAssembly 是静态类型语言,它没必要添加这一开销。WebAssembly 也期望传的值在特定的地方 - 寄存器,而不是 Java 常用的栈里面。

  如果引擎获取一个来自 Java 的参数,用箱子封装一下,并把它给 WebAssembly 函数,WebAssembly 并不知道如何使用它。

  因此,在将参数给 WebAssembly 之前,引擎需要”解箱“这个值,然后放在寄存器里面。

  为了执行这个步骤,会再一次使用 C++。尽管我们不需要通过 C++ 将蹦床设置为激活,仍然需要为传递的值做一些准备工作(从 Java 到 WebAssembly)。

  来到中间人这里是一个很大的开销,尤其是对于那些没那么复杂的。因此,减少中间商会更好。

  这就是我们做的事情。我们把 C++ 运行的代码 - 入口存根,让它直接被 JIT 代码调用。入口存根”解箱值“然后放在正确的地方(寄存器)。通过这样做,我们摆脱了 C++ 的蹦床运动。

  我把这看作一个备忘录,引擎不用去 C++ 就可以使用。相反,当引擎在 WebAssembly 调用 Java 函数的时候,会”解箱“值。

  但在某些情况下,可以更快。事实上,我们可以做到,在某些情况下,Java 到 WebAssembly 的调用比 Java 到 Java 的调用还快。

  当一个 Java 调用另一个 Java 函数的时候,它不知道另一个期望什么样的参数。因此,默认对传入的参数做“封箱”操作。

  但是,如果 Java 函数知道它每次调用的函数每次传入的参数类型都是一样的会怎么样?Java 函数就可以提前按所期望的方式打包参数。

  这是通用 JS JIT 优化的实例之一 - 类型特殊化(type specialization)。 当一个函数特殊化,它能确切知道调用的函数期望什么类型的参数。这意味着它可以提前准备参数…,意味着引擎不再需要备忘单和在“解箱”上面花费额外的开销。

  这种调用 - 每次都调用同样的函数 - 被称为“单一状态的调用”。在 Java 中,对于一个单一状态的调用,你需要每次用相同类型的参数调用这个函数。但是 WebAssembly 函数有明确的类型,调用代码不需要担心参数类型是否一致 - 它们会用强迫的方式让你每次都传相同类型的参数。

  如果你能组织你的代码始终用相同类型的参数,调用 WebAssembly 导出的函数,那么你的调用将会非常快。实际上,这些调用比很多 Java 调用 Java 还要快。

  这里有一个例外的情况,优化过的 Java 》》WebAssembly 并不比 Java 》》Java 快。就是 Java 有内联化函数的时候。

  内联化基本的概念是当你有一个函数一遍又一遍的调用相同的函数时,你可以有更大的捷径。编译器直接复制一份放在调用的地方,而不是让 引擎去和其他的函数沟通。这意味着引擎每必要到处跑 - 自需要待在原地,执行计算。

  当一个函数被调用多次时,这是一个 Java 引擎所做的优化 - 当它“hot”- 并且当函数调用次数相对少的时候。

  我们很明确要在未来对内联化的 WebAssembly 到 Java 中添加支持,并且这也是为什么两种语言在一个引擎上工作的很好的原因。这意味着在后台它们可以使用同意的 JIT 和相同的编译器中间表示形式,因此它们之间可以进行交互操作,如果它们分割在不同的引擎,交互操作根本不可能。

  这里有一种调用比较慢:当 WebAssembly 函数调用 JS 内置函数时。

  内置函数 是浏览器提供的, 像 Math.random。很容易忘记它们也是可以被调用的普通函数。

  有时候内置函数是由 Java 本身实现的,这种情况被称为自托管(self-hosted)。这可以让它们执行的很快,因为意味着你不需要通过 C++:所有的都是通过 Java 执行的。但是有些函数只是在用 C++ 实现的时候执行的更快。

  不同的引擎对于哪些内置函数用 JS 实现,哪些由 c++ 实现有自己的策略。引擎经常混合使用两种语言编写内置函数。

  在用 Java 编写的内置函数的情况下,会受益于我们谈论的所有的优化。但是使用 C++ 编写的函数,我们会后退不得不使用蹦床。

  这些函数会被调用很多次,因此你想要调用优化。为了让调用更快些,我们为内置的函数添加了特定的路径。当你传递一个内置的函数到 WebAssembly 中,引擎会发现你传递的是一个内置函数,这个时候它会知道如何获取快速路径。这意味着你没必要通过蹦床。

  就好像我们建造了一个通往内置大洲的桥。如果你从 WebAssembly 到 内置函数你可以使用那个桥。(旁注:JIT 已经为这种情况做了优化,尽管没在图上显示。)

  目前唯一支持的仅限于 math 的内置函数。因为 WebAssembly 当前只支持整型和浮点型作为值类型。

  math 类的函数工作的很好,因为它们都处理的是数字,但其他内置的函数如 DOM 类的工作并不好。因此当前你想调用它们其中一个函数,你不得不使用 Java。这就是 wasm-bindgen 做的事情。

  我们实施的优化 Math 内置函数的基础设施可以扩展到其他的内置函数,这将会保证很多内置函数更快。

  仍然有一些内置函数需要通过 Java,例如,调用这些内置函数,如果它们使用了 new 关键字或者如果它们使用了 getter 或者 setter。剩余的内置函数将会通过 宿主绑定提案解决。

  这就是我们在 FireFox 中关于 Java 和 WebAssembly 之间调用所做的优化,你应该会很快在其他的浏览器中看到了。

本文链接:http://olivierlutaud.net/diaoyong/546.html
随机为您推荐歌词

联系我们 | 关于我们 | 网友投稿 | 版权声明 | 广告服务 | 站点统计 | 网站地图

版权声明:本站资源均来自互联网,如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

Copyright @ 2012-2013 织梦猫 版权所有  Powered by Dedecms 5.7
渝ICP备10013703号  

回顶部