初探新的 JavaScript 并行特性

简介——我们给 JavaScript 添加了一个 API,开发者可以在 JavaScript 中使用多个 worker 和共享内存来实现真正的并行算法。

多核计算

现如今,JavaScript(JS)已经获得了广泛应用,每个现代网页都包含大量 JS 代码,我们也从未有过顾虑——因为所有的代码都运行在一个进程中。除了网页,JS 还被用于许多高计算量的任务中:(Facebook 和 Lightroom)使用 JS 做服务端图片处理;类似 Google Docs 这样的浏览器端办公套件用 JS 开发;Firefox 的许多组件(比如内置的 PDF 阅读器、pdf.js 以及分词工具)也用 JS 开发。这些应用中有几个实际上使用的是 asm.js,这是一个简单的 JS 子集,可以由 C++ 编译器生成;原本使用 C++ 开发的游戏引擎可以被重新编译成 JS 并通过 asm.js 运行在网页中。

JS 引擎中的 Just-in-Time(JIT)编译器配合性能更强的 CPU 可以为上述任务以及类似的任务带来极大的性能提升。

但是目前 JS 的 JIT 发展缓慢,CPU 性能提升也遇到了瓶颈。由于没有更强的 CPU,所有的电子设备——从电脑系统到智能手机——都选择了多 CPU(多核)作为解决方案。除了低端设备,其他大部分设备都有超过两个核心。对于那些想要提高程序性能的开发者来说,他们需要并行使用多个核心。对于“原生”应用来说这不是什么难题,因为原生应用使用的语言本来就支持多线程(Java、Swift、C# 和 C++)。不幸的是,JS 对多核的支持很差,开发者能用的东西很少(web worker、低效的消息传递和少数几种避免数据拷贝的方法)。

因此,如果我们想让 JS 应用有足够能力和原生应用竞争,就必须让它充分利用多核。

基础设施:共享内存,原子性,Web Worker

在过去一年中,Mozilla 的 JS 团队一直致力于构建 JS 多核计算的基础设施。其他浏览器厂商也参与到了这项工作中,我们的提案已经进入JS 标准化流程。在这个过程中,我们在 Mozilla 的 JS 引擎中实现的原型起了很大作用,并且已经可以在某些版本的 Firefox 中使用。

为了保持Web 可扩展性,我们在实现多核计算底层的基础设施时,尽量减少它们对程序的限制。最终我们实现了三个基础设施:一种新的共享内存的类型、对共享类型对象的原子操作以及一种在标准 web worker 之间传递共享内存对象的方法。这些想法并不是我们首创,Dave Herman 的这篇博文中有更多背景知识和发展历史。

这种新的共享内存类型被称为SharedArrayBuffer,和现在的ArrayBuffer类型很相似,两者最大的区别是:SharedArrayBuffer对应的内存可以被多个代理者同时引用(代理者可以是网页的主程序,也可以是其中一个 web worker)。使用PostMessage在两个代理者之间传递SharedArrayBuffer就会触发共享:

let sab = new SharedArrayBuffer(1024)
let w = new Worker("...")
w.postMessage(sab, [sab])   // 传递 buffer

Worker 会通过消息收到被共享的数据:

let mem;
onmessage = function (ev) { mem = ev.data; }

这就引出了下面这种情况,主程序和 worker 引用了同样的内存,但是这块内存并不属于两种中的任何一个:

一旦SharedArrayBuffer被共享,所有引用它的代理人都可以创建一个TypedArray视图并使用标准的数组操作来读写这块内存。假设 worker 做了下面的操作:

let ia = new Int32Array(mem);
ia[0] = 37;

如果主程序在 worker 写入之后读取第一个元素,它会看到那里写着“37”。

那么,要怎么让主程序“等待 worker 写入”呢?如果多个代理人随意读写同一块内存,那就乱套了。我们需要一套全新的原子操作,从而保证程序对内存的操作按照预定的顺序发生,不会出岔子。原子操作是一组静态方法,存放在一个新的顶层Atomics对象中。

性能和响应度

使用多核计算可以解决两个问题:第一个是性能,也就是单位时间内我们可以完成的工作量;第二个是响应度,也就是浏览器在计算时还能在多大程度上响应用户交互。

我们可以把任务分配给多个并行 worker,从而提高性能:如果我们能把一次计算任务分成四部分,分别运行在四个 worker 上,并且让每个 worker 运行在一个核上,那理论上就可以提速四倍。对于响应度,我们可以把任务从主程序转移到 worker 中,这样主程序可以在不影响任务运行的前提下响应 UI 事件。

之所以需要共享内存,有两个原因。首先,它可以避免数据拷贝。举个例子,如果我们在多个 worker 中渲染一个场景,渲染完毕之后通过主程序展示,那就必须把渲染后的场景拷贝到主程序中,这会增加渲染时间,并且降低主程序的响应度。其次,共享内存会大大降低代理人之间的协作成本,比postMessage要高效得多,这样就可以降低代理人的通信等待时间。

免费午餐?想都别想

多核不是那么好驾驭的。针对单核编写的程序通常需要大幅重构,而且很难验证重构之后程序的正确性。如果 worker 之间需要频繁通信,那就很难发挥多核的性能。并不是所有程序都适合并行。

此外,并行程序会带来许多全新的 bug。如果不小心让两个 worker 互相等待,那程序就无法继续运行:它死锁(deadlock)了。如果 worker 随意读写同样的内存单元,那可能会(无意中并且没有任何征兆地)产生错误数据:程序中出现了数据竞争(data race)。有数据竞争的程序基本可以确定是不正确并且不可靠的。

举个例子

注意: 如果要运行本文中的示例代码,你需要安装 Firefox 46 或者更新的版本。你还需要在about:config页面中把javascript.options.shared_memory设置成true,除非你使用的是Firefox Nightly

我们看看如何让程序通过多核并行来提高性能。下面我们来编写一个简单的分形动画,这个动画需要计算一组像素值并展示在 canvas 中,与此同时不断放大图像。(分形计算通常被认为“极易并行”:很容易通过并行提速。不过事情往往没有你想的那么简单。)这里我们只讨论并行相关的内容,如果你想了解更多信息,可以点击阅读每节结尾列出的链接。

为什么 Firefox 默认关闭了共享内存特性?因为目前它还没有正式成为 JS 标准。成为标准还需要一段时间,这个特性也可能会继续发生变化,我们不希望任何代码依赖现在的 API。

串行分形

我们先来看看不应用并行的分形程序:计算在页面的主程序中进行,直接把结果渲染到 canvas 中。(运行下面的示例代码时,不要持续太长时间,时间越长越卡。)

<iframe src="https://axis-of-eval.org/blog/mandel0.html" height="560" width="660"></iframe>

下面是源代码:

  • 主程序
  • 分形程序

并行分形

并行版本的分型程序会用多个 worker 在一块共享内存中计算像素。从串行变成并行并不难:把mandelbrot方法放到多个 worker 中,每个 worker 计算总像素的一部分。主程序可以在展示 canvas 图像的同时保持响应。

<iframe src="https://axis-of-eval.org/blog/mandel3.html?numWorkers=4" height="560" width="660"></iframe>

下图显示了不同核心数对应的帧率(FPS,每秒帧数)。我们使用的是一台 late-2013 的 MacBook Pro,有四个超线程核心(hyperthreaded core),浏览器是 Firefox 46.0。

核心从一到四的过程中,程序的性能提升基本上是线性的,从 6.9 FPS 增加到了 25.4 FPS。从四核开始,性能提升开始减速,因为程序并不是运行在新的核心上,而是运行在(已被使用的)核心的超线程上。(同一个核心的超线程会共享一些资源,这些资源可能有冲突,从而影响性能。)尽管如此,每增加一个超线程,我们都可以提高 3~4 FPS,增加到 8 个 worker 时,程序的计算速度达到 39.3 FPS,单个核心只有 5.7 FPS。

这种提速效果相当显著。然而,并行版本比串行版本复杂得多。这种复杂性由很多因素导致:

  • 如果并行版本要正常工作,那就必须同步(synchronize) worker 和主程序:主程序必须通知 worker 何时(以及如何)计算,worker 必须通知主程序何时展示结果。当然,可以使用postMessage双向传递数据,但是通常来说用共享内存会更快。快速、正确地实现同步操作真的很难。
  • 划分计算任务需要一套负载均衡(load balancing)策略来充分利用 worker。在这个示例程序中就是如此,输出的图片被划分成了许多块,比 worker 数量多得多。
  • 最后,共享内存本身只是一个一维整数数组;如果要在共享内存中使用更加复杂的数据结构,必须手动处理。

说说同步吧:新的Atomics对象有两个方法,waitwake,可以向 worker 发送信号:一个 worker 通过调用Atomics.wait来等待一个信号,另一个 worker 使用Atomics.wake发送这个信号。不过这些只是可扩展的底层基础设施;要实现同步,程序需要使用其他的原子操作,比如Atomics.loadAtomics.storeAtomics.compareExchange,从而读写共享内存中的状态值。

更复杂的是,网页的主线程不允许调用Atomics.wait,因为主线程不能阻塞(译者注:如果阻塞页面就无法响应用户操作,直接卡死)。所以虽然 worker 可以用Atomics.waitAtomics.wake通信,主线程必须通过监听事件来实现等待,如果想唤醒主线程,worker 必须使用postMessage发送对应的事件。

(提个醒,在 Firefox 46 和 Firefox 47 中,waitwake的名字是futexWaitfutexWake。详情参见Atomics 的 MDN 页面。)

设计良好的库可以隐藏绝大部分复杂性,如果一个程序——或者一个程序的重要部分——可以利用多核大幅提高性能,那所有的付出都是值得的。不过请注意,并行程序并不是万灵药,它无法拯救烂程序。


往期精选文章

ES6中一些超级好用的内置方法

浅谈web自适应

使用Three.js制作酷炫无比的无穷隧道特效

一个治愈JavaScript疲劳的学习计划

全栈工程师技能大全

WEB前端性能优化常见方法

一小时内搭建一个全栈Web应用框架

干货:CSS 专业技巧

四步实现React页面过渡动画效果

让你分分钟理解 JavaScript 闭包



小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。

本文分享自微信公众号 - 京程一灯(jingchengyideng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-09-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏finleyMa

RxJS 5 到 6迁移指导

原文: https://rxjs-dev.firebaseapp.com/guide/v6/migration 转载地址: https://segment...

19620
来自专栏JavaEdge

React.js 实战 - 组件 & Props

组件可以将UI切分成一些独立的、可复用的部件,这样你就只需专注于构建每一个单独的部件.

11710
来自专栏码匠的流水账

聊聊flink的Execution Plan Visualization

本文主要研究一下flink的Execution Plan Visualization

14820
来自专栏前端vue

vue-cli3项目创建-配置-发布

(2) 修改user module -- src/store/module/user.js

4.3K40
来自专栏python教程

基于django的视频点播网站开发-step9-后台视频管理功能

从本讲开始,我们开始视频管理功能的开发,视频管理包括视频上传、视频列表、视频编辑、视频删除。另外还有视频分类的功能,会一同讲解。这一讲非常重要,因为你将学习到一...

24230
来自专栏未闻Code

一日一技:如何从Elasticsearch读取极大量的数据

在使用Elasticsearch时,如果要返回少量的数据,我们可以在DSL语句中指定size这个参数来设定返回多少条数据:

36920
来自专栏KEN DO EVERTHING

快速入门Vue

刚进公司做的第一个项目,刚好前端人手不足,需要我们后端同时兼顾前后端的工作,采用的iview UI框架,基于vue.js。

16910
来自专栏hotqin888的专栏

用beego vue.js element axios 写flow办公流程——系列五

自己的认识:一定要用独立的前端,即vue.js前端项目必须是独立的,独立的服务,不要放beego里的view里作为tpl页面。虽然,放beego view里的t...

16400
来自专栏编程软文

分布式阿波罗Apollo配置中心

为什么要使用apollo,在我们开发分布式微服务项目的时候,那些配置一旦变更,就需要重启服务,这样非常不友好。因此我们考虑动态更改配置文件当中的配置,所以把那些...

29620
来自专栏进击的全栈

认识Set和Map数据结构

tips : 由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致,而entries方法返回的...

17570

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励