你不懂js系列学习笔记-异步与性能- 05

第五章: 程序性能

原文:You-Dont-Know-JSjavascript

这本书至此一直是关于如何更有效地利用异步模式。可是咱们尚未直接解释为何异步对于 JS 如此重要。最明显明确的理由就是 性能html

举个例子,若是你要发起两个 Ajax 请求,并且他们是相互独立的,但你在进行下一个任务以前须要等到他们所有完成,你就有两种选择来对这种互动创建模型:顺序和并发。html5

你能够发起第一个请求并等到它完成再发起第二个请求。或者,就像咱们在 promise 和 generator 中看到的那样,你能够“并列地”发起两个请求,并在继续下一步以前让一个“门”等待它们所有完成。java

显然,后者要比前者性能更好。而更好的性能通常都会带来更好的用户体验。node

异步(并发穿插)甚至可能仅仅加强高性能的印象,即使整个程序依然要用相同的时间才成完成。用户对性能的印象意味着一切——若是不能再多的话!——和实际可测量的性能同样重要。git

如今,咱们想超越局部的异步模式,转而在程序级别的水平上讨论一些宏观的性能细节。github

注意: 你可能会想知道关于微性能问题,好比a++++a哪一个更快。咱们会在下一章“基准分析与调优”中讨论这类性能细节。web

1. Web Workers

若是你有一些处理密集型的任务,但你不想让它们在主线程上运行(那样会使浏览器/UI 变慢),你可能会但愿 JavaScript 能够以多线程的方式操做。算法

在第一章中,咱们详细地谈到了关于 JavaScript 如何是单线程的。那仍然是成立的。可是单线程不是组织你程序运行的惟一方法。编程

想象将你的程序分割成两块儿,在 UI 主线程上运行其中的一起,而在一个彻底分离的线程上运行另外一块儿。

这样的结构会引起什么咱们须要关心的问题?

其一,你会想知道运行在一个分离的线程上是否意味着它在并行运行(在多 CPU/内核的系统上),如此在第二个线程上长时间运行的处理将 不会 阻塞主程序线程。不然,“虚拟线程”所带来的好处,不会比咱们已经在异步并发的 JS 中获得的更多。

并且你会想知道这两块儿程序是否访问共享的做用域/资源。若是是,那么你就要对付多线程语言(Java,C++等等)的全部问题,好比协做式或抢占式锁定(互斥,等)。这是不少额外的工做,并且不该当轻易着手。

换一个角度,若是这两块儿程序不能共享做用域/资源,你会想知道它们将如何“通讯”。

全部这些咱们须要考虑的问题,指引咱们探索一个在近 HTML5 时代被加入 web 平台的特性,称为“Web Worker”。这是一个浏览器(也就是宿主环境)特性,并且几乎和 JS 语言自己没有任何关系。也就是说,JavaScript 当前 并无任何特性能够支持多线程运行。

可是一个像你的浏览器那样的环境能够很容易地提供多个 JavaScript 引擎实例,每一个都在本身的线程上,并容许你在每一个线程上运行不一样的程序。你的程序中分离的线程块儿中的每个都称为一个“(Web)Worker”。这种并行机制叫作“任务并行机制”,它强调将你的程序分割成块儿来并行运行。

在你的主 JS 程序(或另外一个 Worker)中,你能够这样初始化一个 Worker:

var w1 = new Worker("http://some.url.1/mycoolworker.js");
复制代码

这个 URL 应当指向 JS 文件的位置(不是一个 HTML 网页!),它将会被加载到一个 Worker。而后浏览器会启动一个分离的线程,让这个文件在这个线程上做为独立的程序运行。

注意: 这种用这样的 URL 建立的 Worker 称为“专用(Dedicated)Wroker”。但与提供一个外部文件的 URL 不一样的是,你也能够经过提供一个 Blob URL(另外一个 HTML5 特性)来建立一个“内联(Inline)Worker”;它实质上是一个存储在单一(二进制)值中的内联文件。可是,Blob 超出了咱们要在这里讨论的范围。

Worker 不会相互,或者与主程序共享任何做用域或资源——那会将全部的多线程编程的噩梦带到咱们面前——取而代之的是一种链接它们的基本事件消息机制。

w1Worker 对象是一个事件监听器和触发器,它容许你监听 Worker 发出的事件也容许你向 Worker 发送事件。

这是如何监听事件(实际上,是固定的"message"事件):

w1.addEventListener("message", function(evt) {
  // evt.data
});
复制代码

并且你能够发送"message"事件给 Worker:

w1.postMessage("something cool to say");
复制代码

在 Worker 内部,消息是彻底对称的:

// "mycoolworker.js"

addEventListener("message", function(evt) {
  // evt.data
});

postMessage("a really cool reply");
复制代码

要注意的是,一个专用 Worker 与它建立的程序是一对一的关系。也就是,"message"事件不须要消除任何歧义,由于咱们能够肯定它只可能来自于这种一对一关系——不是从 Wroker 来的,就是从主页面来的。

一般主页面的程序会建立 Worker,可是一个 Worker 能够根据须要初始化它本身的子 Worker——称为 subworker。有时将这样的细节委托给一个“主”Worker 十分有用,它能够生成其余 Worker 来处理任务的一部分。不幸的是,在本书写做的时候,Chrome 尚未支持 subworker,然而 Firefox 支持。

要从建立一个 Worker 的程序中当即杀死它,能够在 Worker 对象(就像前一个代码段中的w1)上调用terminate()。忽然终结一个 Worker 线程不会给它任何机会结束它的工做,或清理任何资源。这和你关闭浏览器的标签页来杀死一个页面类似。

若是你在浏览器中有两个或多个页面(或者打开同一个页面的多个标签页!),试着从同一个文件 URL 中建立 Worker,实际上最终结果是彻底分离的 Worker。待一下子咱们就会讨论“共享”Worker 的方法。

注意: 看起来一个恶意的或者是呆头呆脑的 JS 程序能够很容易地经过在系统上生成数百个 Worker 来发起拒绝服务攻击(Dos 攻击),看起来每一个 Worker 都在本身的线程上。虽然一个 Worker 将会在存在于一个分离的线程上是有某种保证的,但这种保证不是没有限制的。系统能够自由决定有多少实际的线程/CPU/内核要去建立。没有办法预测或保证你能访问多少,虽然不少人假定它至少和可用的 CPU/内核数同样多。我认为最安全的臆测是,除了主 UI 线程外至少有一个线程,仅此而已。

Worker 环境

在 Worker 内部,你不能访问主程序的任何资源。这意味着你不能访问它的任何全局变量,你也不能访问页面的 DOM 或其余资源。记住:它是一个彻底分离的线程。

然而,你能够实施网络操做(Ajax,WebSocket)和设置定时器。另外,Worker 能够访问它本身的几个重要全局变量/特性的拷贝,包括navigatorlocationJSON,和applicationCache

你还可使用importScripts(..)加载额外的 JS 脚本到你的 Worker 中:

// 在Worker内部
importScripts("foo.js", "bar.js");
复制代码

这些脚本会被同步地加载,这意味着在文件完成加载和运行以前,importScripts(..)调用会阻塞 Worker 的执行。

注意: 还有一些关于暴露<canvas>API 给 Worker 的讨论,其中包括使 canvas 成为 Transferable 的(见“数据传送”一节),这将容许 Worker 来实施一些精细的脱线程图形处理,在高性能的游戏(WebGL)和其余相似应用中可能颇有用。虽然这在任何浏览器中都还不存在,可是颇有可能在近将来发生。

Web Worker 的常见用途是什么?

  • 处理密集型的数学计算
  • 大数据集合的排序
  • 数据操做(压缩,音频分析,图像像素操做等等)
  • 高流量网络通讯

数据传送

你可能注意到了这些用途中的大多数的一个共同性质,就是它们要求使用事件机制穿越线程间的壁垒来传递大量的信息,也许是双向的。

在 Worker 的早期,将全部数据序列化为字符串是惟一的选择。除了在两个方向上进行序列化时速度上变慢了,另一个主要缺点是,数据是被拷贝的,这意味着内存用量翻了一倍(以及在后续垃圾回收上的流失)。

谢天谢地,如今咱们有了几个更好的选择。

若是你传递一个对象,在另外一端一个所谓的“结构化克隆算法(Structured Cloning Algorithm)”(developer.mozilla.org/en-US/docs/… )会用于拷贝/复制这个对象。这个算法至关精巧,甚至能够处理带有循环引用的对象复制。to-string/from-string 的性能劣化没有了,但用这种方式咱们依然面对着内存用量的翻倍。IE10 以上版本,和其余主流浏览器都对此有支持。

一个更好的选择,特别是对大的数据集合而言,是“Transferable 对象”(updates.html5rocks.com/2011/12/Tra… )。它使对象的“全部权”被传送,而对象自己没动。一旦你传送一个对象给 Worker,它在原来的位置就空了出来或者不可访问——这消除了共享做用域的多线程编程中的灾难。固然,全部权的传送能够双向进行。

选择使用 Transferable 对象不须要你作太多;任何实现了 Transferable 接口(developer.mozilla.org/en-US/docs/… )的数据结构都将自动地以这种方式传递(Firefox 和 Chrome 支持此特性)。

举个例子,有类型的数组如Uint8Array(见本系列的 ES6 与将来)是一个“Transferables”。这是你如何用postMessage(..)来传送一个 Transferable 对象:

// `foo` 是一个 `Uint8Array`

postMessage(foo.buffer, [foo.buffer]);
复制代码

第一个参数是未经加工的缓冲,而第二个参数是要传送的内容的列表。

不支持 Transferable 对象的浏览器简单地降级到结构化克隆,这意味着性能上的下降,而不是完全的特性失灵。

2. SIMD

一个指令,多个数据(SIMD)是一种“数据并行机制”形式,与 Web Worker 的“任务并行机制”相对应,由于他强调的不是程序逻辑的块儿被并行化,而是多个字节的数据被并行地处理。

使用 SIMD,线程不提供并行机制。相反,现代 CPU 用数字的“向量”提供 SIMD 能力——想一想:指定类型的数组——还有能够在全部这些数字上并行操做的指令;这些是利用底层操做的指令级别的并行机制。

使 SIMD 能力包含在 JavaScript 中的努力主要是由 Intel 带头的(01.org/node/1495 ),名义上是 Mohammad Haghighat(在本书写做的时候),与 Firefox 和 Chrome 团队合做。SIMD 处于早期标准化阶段,并且颇有可能被加入将来版本的 JavaScript 中,极可能在 ES7 的时间框架内。

SIMD JavaScript 提议向 JS 代码暴露短向量类型与 API,它们在 SIMD 可用的系统中将操做直接映射为 CPU 指令的等价物,同时在非 SIMD 系统中退回到非并行化操做的“shim”。

对于数据密集型的应用程序(信号分析,对图形的矩阵操做等等)来讲,这种并行数学处理在性能上的优点是十分明显的!

在本书写做时,SIMD API 的早期提案形式看起来像这样:

var v1 = SIMD.float32x4(3.14159, 21.0, 32.3, 55.55);
var v2 = SIMD.float32x4(2.1, 3.2, 4.3, 5.4);

var v3 = SIMD.int32x4(10, 101, 1001, 10001);
var v4 = SIMD.int32x4(10, 20, 30, 40);

SIMD.float32x4.mul(v1, v2); // [ 6.597339, 67.2, 138.89, 299.97 ]
SIMD.int32x4.add(v3, v4); // [ 20, 121, 1031, 10041 ]
复制代码

这里展现了两种不一样的向量数据类型,32 位浮点数和 32 位整数。你能够看到这些向量正好被设置为 4 个 32 位元素,这与大多数 CPU 中可用的 SIMD 向量的大小(128 位)相匹配。在将来咱们看到一个x8(或更大!)版本的这些 API 也是可能的。

除了mul()add(),许多其余操做也极可能被加入,好比sub()div()abs()neg()sqrt()reciprocal()reciprocalSqrt() (算数运算),shuffle()(重拍向量元素),and()or()xor()not()(逻辑运算),equal()greaterThan()lessThan() (比较运算),shiftLeft()shiftRightLogical()shiftRightArithmetic()(轮换),fromFloat32x4(),和fromInt32x4()(变换)。

注意: 这里有一个 SIMD 功能的官方“填补”(颇有但愿,预期的,着眼将来的填补)(github.com/johnmccutch… ),它描述了许多比咱们在这一节中没有讲到的许多计划中的 SIMD 功能。

3. asm.js

“asm.js”(asmjs.org/ )是能够被高度优化的 JavaScript 语言子集的标志。经过当心地回避那些特定的很难优化的(垃圾回收,强制转换,等等)机制和模式,asm.js 风格的代码能够被 JS 引擎识别,并且用主动地底层优化进行特殊的处理。

与本章中讨论的其余性能优化机制不一样的是,asm.js 没必需要是必须被 JS 语言规范所采纳的东西。确实有一个 asm.js 规范(asmjs.org/spec/latest… ),但它主要是追踪一组关于优化的候选对象的推论,而不是 JS 引擎的需求。

目前尚未新的语法被提案。取而代之的是,ams.js 建议了一些方法,用来识别那些符合 ams.js 规则的既存标准 JS 语法,而且让引擎相应地实现它们本身的优化功能。

关于 ams.js 应当如何在程序中活动的问题,在浏览器生产商之间存在一些争议。早期版本的 asm.js 实验中,要求一个"use asm";编译附注(与 strict 模式的"use strict";相似)来帮助 JS 引擎来寻找 asm.js 优化的机会和提示。另外一些人则断言 asm.js 应当只是一组启发式算法,让引擎自动地识别而不用做者作任何额外的事情,这意味着理论上既存的程序能够在不用作任何特殊的事情的状况下从 asm.js 优化中获益。

如何使用 asm.js 进行优化

关于 asm.js 须要理解的第一件事情是类型和强制转换。若是 JS 引擎不得不在变量的操做期间一直追踪一个变量内的值的类型,以便于在必要时它能够处理强制转换,那么就会有许多额外的工做使程序处于次优化状态。

注意: 为了说明的目的,咱们将在这里使用 ams.js 风格的代码,但要意识到的是你手写这些代码的状况不是很常见。asm.js 的本意更多的是做为其余工具的编译目标,好比 Emscripten(github.com/kripken/ems… )。固然你写本身的 asm.js 代码也是可能的,可是这一般不是一个好主意,由于那样的代码很是底层,而这意味着它会很是耗时并且易错。尽管如此,也会有状况使你想要为了 ams.js 优化的目的手动调整代码。

这里有一些“技巧”,你可使用它们来提示支持 asm.js 的 JS 引擎变量/操做预期的类型是什么,以便于它能够跳过那些强制转换追踪的步骤。

举个例子:

var a = 42;

// ..

var b = a;
复制代码

在这个程序中,赋值b = a在变量中留下了类型分歧的问题。然而,它能够写成这样:

var a = 42;

// ..

var b = a | 0;
复制代码

这里,咱们与值0一块儿使用了|(“二进制或”),虽然它对值没有任何影响,但它确保这个值是一个 32 位整数。这段代码在普通的 JS 引擎中能够工做,可是当它运行在支持 asm.js 的 JS 引擎上时,它 能够 表示b应当老是被做为 32 位整数来对待,因此强制转换追踪能够被跳过。

相似地,两个变量之间的加法操做能够被限定为性能更好的整数加法(而不是浮点数):

(a + b) | 0;
复制代码

再一次,支持 asm.js 的 JS 引擎能够看到这个提示,并推断+操做应当是一个 32 位整数加法,由于不论怎样整个表达式的最终结果都将自动是 32 位整数。

复习

本书的前四章基于这样的前提:异步编码模式给了你编写更高效代码的能力,这一般是一个很是重要的改进。可是异步行为也就能帮你这么多,由于它在基础上仍然使用一个单独的事件轮询线程。

因此在这一章咱们涵盖了几种程序级别的机制来进一步提高性能。

Web Worker 让你在一个分离的线程上运行一个 JS 文件(也就是程序),使用异步事件在线程之间传递消息。对于将长时间运行或资源密集型任务挂载到一个不一样线程,从而让主 UI 线程保持相应来讲,它们很是棒。

SIMD 提议将 CPU 级别的并行数学操做映射到 JavaScript API 上来提供高性能数据并行操做,好比在大数据集合上进行数字处理。

最后,asm.js 描述了一个 JavaScript 的小的子集,它回避了 JS 中不易优化的部分(好比垃圾回收与强制转换)并让 JS 引擎经过主动优化识别并运行这样的代码。asm.js 能够手动编写,可是极其麻烦且易错,就像手动编写汇编语言。相反,asm.js 的主要意图是做为一个从其余高度优化的程序语言交叉编译来的目标——例如,Emscripten(github.com/kripken/ems… )能够将 C/C++转译为 JavaScript。

虽然在本章没有明确地说起,在很早之前的有关 JavaScript 的讨论中存在着更激进的想法,包括近似地直接多线程功能(不只仅是隐藏在数据结构 API 后面)。不管这是否会明确地发生,仍是咱们将看到更多并行机制偷偷潜入 JS,可是在 JS 中发生更多程序级别优化的将来是能够肯定的。

相关文章
相关标签/搜索