V8 JavaScript 引擎:高性能的 ES2015+

AdelineUrib 7年前
   <p>在过去的几个月中,V8 团队一直努力让新增的 <a href="/misc/goto?guid=4958968604374563640" rel="nofollow,noindex">ES2015</a> 和其它更前沿的 JavaScript 功能的性能达到等效的 <a href="/misc/goto?guid=4959622334776138960" rel="nofollow,noindex">ES5</a> 的水平。</p>    <h2>动机</h2>    <p>在我们详细介绍各种改进之前,我们首先应该考虑为什么 ES2015+ 功能的性能很重要,尽管 <a href="/misc/goto?guid=4958871204393151840" rel="nofollow,noindex">Babel</a> 在现代 Web 开发中得到广泛的应用:</p>    <ol>     <li> <p>首先,有的 ES2015 功能是按需解析成 ES5 的,例如内置的 <a href="/misc/goto?guid=4959749853361118407" rel="nofollow,noindex">Object.assign</a> 。 当 Babel 编译 <a href="/misc/goto?guid=4959749853451293700" rel="nofollow,noindex">对象扩展语法</a> (应用在大量 <a href="/misc/goto?guid=4959749853541642658" rel="nofollow,noindex">React</a> 和 <a href="/misc/goto?guid=4958973395905045282" rel="nofollow,noindex">Redux</a> 程序)并且编译器也支持这个语法时,Babel 会使用 Object.assign 而弃用等效的 ES5 代码。</p> </li>     <li> <p>将 ES2015 功能解析成 ES5 通常会增加大量代码,加剧了当前的 <a href="/misc/goto?guid=4959749853670892808" rel="nofollow,noindex">Web 性能危机</a> ,尤其不利于新兴市场上常见的千元机。因此,即使在考虑实际执行成本之前,传输、解析和编译代码的成本就相当高。</p> </li>     <li> <p>最后,客户端JavaScript只是依赖于V8引擎的环境之一。 还有用于服务器端应用程序和工具的 <a href="/misc/goto?guid=4958867323246690255" rel="nofollow,noindex">Node.js</a> ,开发人员不需要将代码解析成 ES5,可以直接使用目标 Node.js 版本中 <a href="/misc/goto?guid=4959749853786081189" rel="nofollow,noindex">相关 V8 版本</a> 支持的功能。</p> </li>    </ol>    <p>让我们考虑以下节选自 <a href="/misc/goto?guid=4959749853867105600" rel="nofollow,noindex">Redux 文档</a> 中的代码段:</p>    <pre>  <code class="language-javascript">function todoApp(state = initialState, action) {    switch (action.type) {      case SET_VISIBILITY_FILTER:        return { ...state, visibilityFilter: action.filter }      default:        return state    }  }</code></pre>    <p>该代码中有两处需要解析成 ES5: state 的默认参数和 state 的扩展对象语法。Babel 生成以下 ES5 代码:</p>    <pre>  <code class="language-javascript">"use strict";    var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };    function todoApp() {    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;    var action = arguments[1];      switch (action.type) {      case SET_VISIBILITY_FILTER:        return _extends({}, state, { visibilityFilter: action.filter });      default:        return state;    }  }</code></pre>    <p>现在假如 <a href="/misc/goto?guid=4959749853361118407" rel="nofollow,noindex"> Object.assign </a> 比 Babel 生成的 polyfilled_extends 要慢好几个数量级。在这种情况下,从不支持 Object.assign 的浏览器升级到支持 ES2015 的浏览器版本将大幅降低性能,可能会阻碍 ES2015 的普及。</p>    <p>此示例还体现了解析成 ES5 的另一个重要缺点:发送给用户的代码通常远大于开发人员最初编写的 ES2015+ 代码。在上面的示例中,原始代码是 203 字符(gzip 压缩后 176 字节),而生成的代码是 588 字符(gzip 压缩后 367 字节)。体积增长了两倍。 我们来看看 <a href="/misc/goto?guid=4959749853971090322" rel="nofollow,noindex">Async Iterators for JavaScript</a> 的另一个例子:</p>    <pre>  <code class="language-javascript">async function* readLines(path) {    let file = await fileOpen(path);      try {      while (!file.EOF) {        yield await file.readLine();      }    } finally {      await file.close();    }  }</code></pre>    <p>Babel 将以上 187 字符(gzip 压缩后 150 字节)解析成 2987 字符的 ES5 代码(gzip 压缩后 971 字节),这里还没考虑所需依赖的 <a href="/misc/goto?guid=4959749854060865202" rel="nofollow,noindex">regenerator runtime</a> :</p>    <pre>  <code class="language-javascript">"use strict";    var _asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function wrap(fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function await(value) { return new AwaitValue(value); } }; }();    var readLines = function () {    var _ref = _asyncGenerator.wrap(regeneratorRuntime.mark(function _callee(path) {      var file;      return regeneratorRuntime.wrap(function _callee$(_context) {        while (1) {          switch (_context.prev = _context.next) {            case 0:              _context.next = 2;              return _asyncGenerator.await(fileOpen(path));              case 2:              file = _context.sent;              _context.prev = 3;              case 4:              if (file.EOF) {                _context.next = 11;                break;              }                _context.next = 7;              return _asyncGenerator.await(file.readLine());              case 7:              _context.next = 9;              return _context.sent;              case 9:              _context.next = 4;              break;              case 11:              _context.prev = 11;              _context.next = 14;              return _asyncGenerator.await(file.close());              case 14:              return _context.finish(11);              case 15:            case "end":              return _context.stop();          }        }      }, _callee, this, [[3,, 11, 15]]);    }));      return function readLines(_x) {      return _ref.apply(this, arguments);    };  }();</code></pre>    <p>代码体积增加了 <strong>650%</strong> ( _asyncGenerator 函数是可复用的,具体取决于捆绑代码的方式,因此可以在多个异步迭代器使用中减小一些代码的体积)。我们不认为将代码解析成 ES5 可以解决所有问题,因为代码体积的增加不仅会影响下载时间/成本,还会增加解析和编译的额外开销。如果我们真的想大幅度地改善现代 Web 应用程序的页面加载和缓存(特别是在移动设备上)的效率,我们必须鼓励开发人员在编写代码时不仅使用 ES2015+,并且不需解析成 ES5 就直接发送给客户端,只向不支持 ES2015 的传统浏览器提供完全解析的代码。对于编译器的作者而言,这一想法意味着我们需要直接支持 ES2015+ 功能, <strong>并</strong> 提供合理的性能。</p>    <h2>测试方法</h2>    <p>如上所述,ES2015+ 功能的绝对性能并不是主要矛盾。相反,目前应优先确保 ES2015+ 功能的性能与等效的原生 ES5 代码相当,更重要的是和 Babel 生成的代码性能相当。 <a href="/misc/goto?guid=4959749854143897639" rel="nofollow,noindex">Kevin Decker</a> 有一个项目叫 <a href="/misc/goto?guid=4959749854241928481" rel="nofollow,noindex">six-speed</a> ,它或多或少可以满足我们的需求:ES2015 功能与等效的原生 ES5 代码与解析后产生的 ES5 代码之间的性能比较。</p>    <p><img src="https://simg.open-open.com/show/00c00b734bc52e57c6a1c4a9e9376114.png"></p>    <p>所以我们决定用它作为我们开始 ES2015+ 性能工作的基础。我们 <a href="/misc/goto?guid=4959749854320673433" rel="nofollow,noindex">拷贝</a> 了该项目并添加了一些测试。 我们首先关注性能最差的部分,比如说列表项,原生的 ES5 比 ES2015+ 版本效率高 2 倍,因为我们的基本假设是原生的 ES5 版本至少与 Babel 的版本一样快。</p>    <h2>一个为现代语言而生的现代架构</h2>    <p>过去,V8 很难改善 ES2015+ 功能的优化,例如,给 Crankshaft —— V8 的经典优化编译器—— 添加异常处理(比如 try/catch/finally )是不可行的。 这意味着 V8 优化 ES6 功能像 for...of 之类的的能力是有限的,因为它本质上是一个隐含的 finally 子句。Crankshaft 的局限性以及将全新的语言功能添加到全代码(V8 的基准编译器)中的整体复杂性,使得 V8 难添加和优化刚刚标准化的新 ES 功能。</p>    <p>幸运的是, <a href="/misc/goto?guid=4959749854416454210" rel="nofollow,noindex">V8 的新的解释器 Ignition 和编译器管道 TurboFan</a> 从一开始就着手支持整个 JavaScript 语言,包括高级控制流程,异常处理以及 ES2015 的最新版本和解构赋值。Ignition 和 TurboFan 架构的紧密结合可以快速添加新功能并逐步进行优化。</p>    <p>对于许多现代的 ES 功能和改进只有在新的 Ignition 和 TurboFan 下才可行。 Ignition 和 TurboFan 对于优化生成器和 async 尤其重要。生成器早已得到 V8 的支持,但由于 Crankshaft 控制流的限制而不能进一步得到优化。 async 基本上是生成器的语法糖,因此属于同一类别。新的编译器管道利用 Ignition 来实现 AST,并生成可以转换生成器控制流到简单的本地控制流的字节码。TurboFan 可以更容易地优化所得到的字节码,因为它不需要知道关于生成器控制流的任何具体内容,只是如何保存和恢复函数的 yield 状态。</p>    <p><img src="https://simg.open-open.com/show/1664a4fee7f4cd6bcf8e5fc7f4bbb062.png"></p>    <h2>小组的状况</h2>    <p>我们的短期目标是让效率差距尽快缩减到 2 倍以内。我们首先改进测试成绩最差的功能,从 Chrome M54 到 Chrome M58(Canary),我们已经成功将测试速度降了一倍,从 16 降至 8 ,同时 M54 中最差的 19 倍在 M58(Canary)减少到了只有 6 倍。与此同时,我们也大大减少了效率差距的平均和中位数:</p>    <p><img src="https://simg.open-open.com/show/d82e718e2f686b4b08a6848075a9bff1.png"></p>    <p>可以看到 ES2015+ 和 ES5 正在接近的趋势。我们把平均性能提高到了 ES5 的 47% 以上。 以下是自 M54 以来我们做的一些亮点。</p>    <p><img src="https://simg.open-open.com/show/f53b44e95f2aee4199c5a20a68b9d55f.png"></p>    <p>最值得注意的是,我们改进了基于迭代的新语言结构的性能,如扩展运算符,解构赋值和 for...of 循环。例如,使用数组解构赋值</p>    <pre>  <code class="language-javascript">function fn() {    var [c] = data;    return c;  }</code></pre>    <p>和原生的 ES5 赋值语句效率相当</p>    <pre>  <code class="language-javascript">function fn() {    var c = data[0];    return c;  }</code></pre>    <p>比 babel 生成的代码快多了:</p>    <pre>  <code class="language-javascript">"use strict";    var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();    function fn() {    var _data = data,        _data2 = _slicedToArray(_data, 1),        c = _data2[0];      return c;  }</code></pre>    <p>想了解更多详细信息可以在上次 <a href="/misc/goto?guid=4959749854506283738" rel="nofollow,noindex">慕尼黑 NodeJS 用户组会议</a> 上查看我们提供的 <a href="/misc/goto?guid=4959749854592499067" rel="nofollow,noindex">高效 ES2015</a> 演讲:</p>    <p><a href="/misc/goto?guid=4959749854681624420" rel="nofollow,noindex">https://www.油Tube.com/embed/XBSyyxN7Q-o</a></p>    <p>我们致力于继续提高 ES2015+ 的性能。如果对这些细节感兴趣,请查看 <a href="/misc/goto?guid=4959749854776247473" rel="nofollow,noindex">V8 的 ES2015 及其未来的性能计划</a> 。</p>    <p><img src="https://simg.open-open.com/show/93c0bbcb040da3fbcdb62db2aa7547d8.jpg"></p>    <p>来自:https://w3ctech.com/topic/2035</p>    <p> </p>