V8 JavaScript 引擎:高性能的 ES2015+
在過去的幾個月中,V8 團隊一直努力讓新增的 ES2015 和其它更前沿的 JavaScript 功能的性能達到等效的 ES5 的水平。
動機
在我們詳細介紹各種改進之前,我們首先應該考慮為什么 ES2015+ 功能的性能很重要,盡管 Babel 在現代 Web 開發中得到廣泛的應用:
-
首先,有的 ES2015 功能是按需解析成 ES5 的,例如內置的 Object.assign 。 當 Babel 編譯 對象擴展語法 (應用在大量 React 和 Redux 程序)并且編譯器也支持這個語法時,Babel 會使用 Object.assign 而棄用等效的 ES5 代碼。
-
將 ES2015 功能解析成 ES5 通常會增加大量代碼,加劇了當前的 Web 性能危機 ,尤其不利于新興市場上常見的千元機。因此,即使在考慮實際執行成本之前,傳輸、解析和編譯代碼的成本就相當高。
-
最后,客戶端JavaScript只是依賴于V8引擎的環境之一。 還有用于服務器端應用程序和工具的 Node.js ,開發人員不需要將代碼解析成 ES5,可以直接使用目標 Node.js 版本中 相關 V8 版本 支持的功能。
讓我們考慮以下節選自 Redux 文檔 中的代碼段:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return { ...state, visibilityFilter: action.filter }
default:
return state
}
}
該代碼中有兩處需要解析成 ES5: state 的默認參數和 state 的擴展對象語法。Babel 生成以下 ES5 代碼:
"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;
}
}
現在假如 Object.assign 比 Babel 生成的 polyfilled_extends 要慢好幾個數量級。在這種情況下,從不支持 Object.assign 的瀏覽器升級到支持 ES2015 的瀏覽器版本將大幅降低性能,可能會阻礙 ES2015 的普及。
此示例還體現了解析成 ES5 的另一個重要缺點:發送給用戶的代碼通常遠大于開發人員最初編寫的 ES2015+ 代碼。在上面的示例中,原始代碼是 203 字符(gzip 壓縮后 176 字節),而生成的代碼是 588 字符(gzip 壓縮后 367 字節)。體積增長了兩倍。 我們來看看 Async Iterators for JavaScript 的另一個例子:
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
Babel 將以上 187 字符(gzip 壓縮后 150 字節)解析成 2987 字符的 ES5 代碼(gzip 壓縮后 971 字節),這里還沒考慮所需依賴的 regenerator runtime :
"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);
};
}();
代碼體積增加了 650% ( _asyncGenerator 函數是可復用的,具體取決于捆綁代碼的方式,因此可以在多個異步迭代器使用中減小一些代碼的體積)。我們不認為將代碼解析成 ES5 可以解決所有問題,因為代碼體積的增加不僅會影響下載時間/成本,還會增加解析和編譯的額外開銷。如果我們真的想大幅度地改善現代 Web 應用程序的頁面加載和緩存(特別是在移動設備上)的效率,我們必須鼓勵開發人員在編寫代碼時不僅使用 ES2015+,并且不需解析成 ES5 就直接發送給客戶端,只向不支持 ES2015 的傳統瀏覽器提供完全解析的代碼。對于編譯器的作者而言,這一想法意味著我們需要直接支持 ES2015+ 功能, 并 提供合理的性能。
測試方法
如上所述,ES2015+ 功能的絕對性能并不是主要矛盾。相反,目前應優先確保 ES2015+ 功能的性能與等效的原生 ES5 代碼相當,更重要的是和 Babel 生成的代碼性能相當。 Kevin Decker 有一個項目叫 six-speed ,它或多或少可以滿足我們的需求:ES2015 功能與等效的原生 ES5 代碼與解析后產生的 ES5 代碼之間的性能比較。
所以我們決定用它作為我們開始 ES2015+ 性能工作的基礎。我們 拷貝 了該項目并添加了一些測試。 我們首先關注性能最差的部分,比如說列表項,原生的 ES5 比 ES2015+ 版本效率高 2 倍,因為我們的基本假設是原生的 ES5 版本至少與 Babel 的版本一樣快。
一個為現代語言而生的現代架構
過去,V8 很難改善 ES2015+ 功能的優化,例如,給 Crankshaft —— V8 的經典優化編譯器—— 添加異常處理(比如 try/catch/finally )是不可行的。 這意味著 V8 優化 ES6 功能像 for...of 之類的的能力是有限的,因為它本質上是一個隱含的 finally 子句。Crankshaft 的局限性以及將全新的語言功能添加到全代碼(V8 的基準編譯器)中的整體復雜性,使得 V8 難添加和優化剛剛標準化的新 ES 功能。
幸運的是, V8 的新的解釋器 Ignition 和編譯器管道 TurboFan 從一開始就著手支持整個 JavaScript 語言,包括高級控制流程,異常處理以及 ES2015 的最新版本和解構賦值。Ignition 和 TurboFan 架構的緊密結合可以快速添加新功能并逐步進行優化。
對于許多現代的 ES 功能和改進只有在新的 Ignition 和 TurboFan 下才可行。 Ignition 和 TurboFan 對于優化生成器和 async 尤其重要。生成器早已得到 V8 的支持,但由于 Crankshaft 控制流的限制而不能進一步得到優化。 async 基本上是生成器的語法糖,因此屬于同一類別。新的編譯器管道利用 Ignition 來實現 AST,并生成可以轉換生成器控制流到簡單的本地控制流的字節碼。TurboFan 可以更容易地優化所得到的字節碼,因為它不需要知道關于生成器控制流的任何具體內容,只是如何保存和恢復函數的 yield 狀態。
小組的狀況
我們的短期目標是讓效率差距盡快縮減到 2 倍以內。我們首先改進測試成績最差的功能,從 Chrome M54 到 Chrome M58(Canary),我們已經成功將測試速度降了一倍,從 16 降至 8 ,同時 M54 中最差的 19 倍在 M58(Canary)減少到了只有 6 倍。與此同時,我們也大大減少了效率差距的平均和中位數:
可以看到 ES2015+ 和 ES5 正在接近的趨勢。我們把平均性能提高到了 ES5 的 47% 以上。 以下是自 M54 以來我們做的一些亮點。
最值得注意的是,我們改進了基于迭代的新語言結構的性能,如擴展運算符,解構賦值和 for...of 循環。例如,使用數組解構賦值
function fn() {
var [c] = data;
return c;
}
和原生的 ES5 賦值語句效率相當
function fn() {
var c = data[0];
return c;
}
比 babel 生成的代碼快多了:
"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;
}
想了解更多詳細信息可以在上次 慕尼黑 NodeJS 用戶組會議 上查看我們提供的 高效 ES2015 演講:
https://www.油Tube.com/embed/XBSyyxN7Q-o
我們致力于繼續提高 ES2015+ 的性能。如果對這些細節感興趣,請查看 V8 的 ES2015 及其未來的性能計劃 。
來自:https://w3ctech.com/topic/2035