From 0d542b61d3f144a60108e57439bf1a0d9638fbec Mon Sep 17 00:00:00 2001 From: Ingvar Stepanyan Date: Mon, 9 Feb 2015 18:00:07 +0200 Subject: [PATCH] Performance improvements for TCO. http://jsperf.com/tco/17 --- .../templates/tail-call-body.js | 14 +- .../transformers/es6/parameters.default.js | 4 +- .../transformers/es6/tail-call.js | 214 ++++++++++++++---- lib/6to5/transformation/transformers/index.js | 2 + lib/6to5/traversal/scope.js | 2 +- .../es6-tail-call/call-apply/expected.js | 33 +-- .../es6-tail-call/expressions/expected.js | 26 +-- .../es6-tail-call/factorial/actual.js | 3 + .../es6-tail-call/factorial/expected.js | 16 ++ .../es6-tail-call/recursion/actual.js | 12 +- .../es6-tail-call/recursion/expected.js | 39 ++-- .../es6-tail-call/try-catch/expected.js | 46 ++-- 12 files changed, 253 insertions(+), 158 deletions(-) create mode 100644 test/fixtures/transformation/es6-tail-call/factorial/actual.js create mode 100644 test/fixtures/transformation/es6-tail-call/factorial/expected.js diff --git a/lib/6to5/transformation/templates/tail-call-body.js b/lib/6to5/transformation/templates/tail-call-body.js index 2b6d268851..a8391779f8 100644 --- a/lib/6to5/transformation/templates/tail-call-body.js +++ b/lib/6to5/transformation/templates/tail-call-body.js @@ -1,15 +1,3 @@ { - var ARGUMENTS_ID = arguments, - THIS_ID = this, - SHOULD_CONTINUE_ID, - RESULT_ID; - - var CALLEE_ID = FUNCTION; - - do { - SHOULD_CONTINUE_ID = false; - RESULT_ID = CALLEE_ID.apply(THIS_ID, ARGUMENTS_ID); - } while(SHOULD_CONTINUE_ID); - - return RESULT_ID; + FUNCTION_ID:while (true) BLOCK } diff --git a/lib/6to5/transformation/transformers/es6/parameters.default.js b/lib/6to5/transformation/transformers/es6/parameters.default.js index 18c113c46b..040d517ddf 100644 --- a/lib/6to5/transformation/transformers/es6/parameters.default.js +++ b/lib/6to5/transformation/transformers/es6/parameters.default.js @@ -73,7 +73,9 @@ exports.Function = function (node, parent, scope, file) { var left = param.left; var right = param.right; - node.params[i] = scope.generateUidIdentifier("x"); + var placeholder = scope.generateUidIdentifier("x"); + placeholder._isDefaultPlaceholder = true; + node.params[i] = placeholder; if (!state.iife) { if (t.isIdentifier(right) && scope.hasOwnReference(right.name)) { diff --git a/lib/6to5/transformation/transformers/es6/tail-call.js b/lib/6to5/transformation/transformers/es6/tail-call.js index 426a23fc2c..a15f8ca3a0 100644 --- a/lib/6to5/transformation/transformers/es6/tail-call.js +++ b/lib/6to5/transformation/transformers/es6/tail-call.js @@ -1,5 +1,6 @@ "use strict"; +var _ = require("lodash"); var util = require("../../../util"); var t = require("../../../types"); @@ -27,7 +28,7 @@ function transformExpression(node, scope, state) { } else { node.alternate = returnBlock(node.alternate); } - return node; + return [node]; case "LogicalExpression": // only call in right-value of can be optimized @@ -96,71 +97,137 @@ function transformExpression(node, scope, state) { state.hasTailRecursion = true; - return [ - t.expressionStatement(t.assignmentExpression( - "=", - state.getArgumentsId(), - args || t.arrayExpression(node.arguments) - )), + var body = []; - t.expressionStatement(t.assignmentExpression( + if (!t.isThisExpression(thisBinding)) { + body.push(t.expressionStatement(t.assignmentExpression( "=", state.getThisId(), thisBinding || t.identifier("undefined") - )), + ))); + } - t.returnStatement(t.assignmentExpression( - "=", - state.getShouldContinueId(), - t.literal(true) - )) - ]; + if (!args) { + args = t.arrayExpression(node.arguments); + } + + var argumentsId = state.getArgumentsId(); + var params = state.getParams(); + + body.push(t.expressionStatement(t.assignmentExpression( + "=", + argumentsId, + args + ))); + + var i, param; + + if (t.isArrayExpression(args)) { + var elems = args.elements; + for (i = 0; i < elems.length && i < params.length; i++) { + param = params[i]; + var elem = elems[i] || (elems[i] = t.identifier("undefined")); + if (!param._isDefaultPlaceholder) { + elems[i] = t.assignmentExpression("=", param, elem); + } + } + } else { + state.setsArguments = true; + for (i = 0; i < params.length; i++) { + param = params[i]; + if (!param._isDefaultPlaceholder) { + body.push(t.expressionStatement(t.assignmentExpression( + "=", + param, + t.memberExpression(argumentsId, t.literal(i), true) + ))); + } + } + } + + body.push(t.continueStatement(state.getFunctionId())); + + return body; } })(node); } -var functionChildrenVisitor = { +// Looks for and replaces tail recursion calls. +var firstPass = { enter: function (node, parent, scope, state) { if (t.isReturnStatement(node)) { - // prevent entrance by current visitor this.skip(); - - // transform return argument into statement if - // it contains tail recursion return transformExpression(node.argument, scope, state); - } else if (t.isFunction(node)) { - this.skip(); } else if (t.isTryStatement(parent)) { if (node === parent.block) { this.skip(); } else if (parent.finalizer && node !== parent.finalizer) { this.skip(); } + } else if (t.isFunction(node)) { + this.skip(); + } else if (t.isVariableDeclaration(node)) { + this.skip(); + state.vars.push(node); } } }; -var functionVisitor = { +// Hoists up function declarations, replaces `this` and `arguments` and +// marks them as needed. +var secondPass = { enter: function (node, parent, scope, state) { - // traverse all child nodes of this function and find `arguments` and `this` - scope.traverse(node, functionChildrenVisitor, state); - - return this.skip(); + if (t.isThisExpression(node)) { + state.needsThis = true; + return state.getThisId(); + } else if (t.isReferencedIdentifier(node, parent, { name: "arguments" })) { + state.needsArguments = true; + return state.getArgumentsId(); + } else if (t.isFunction(node)) { + this.skip(); + if (t.isFunctionDeclaration(node)) { + node = t.variableDeclaration("var", [ + t.variableDeclarator(node.id, t.toExpression(node)) + ]); + node._blockHoist = 2; + return node; + } + } } }; -exports.FunctionDeclaration = -exports.FunctionExpression = function (node, parent, scope) { +// Optimizes recursion by removing `this` and `arguments` +// if they are not used. +var thirdPass = { + enter: function (node, parent, scope, state) { + if (!t.isExpressionStatement(node)) return; + var expr = node.expression; + if (!t.isAssignmentExpression(expr)) return; + if (!state.needsThis && expr.left === state.getThisId()) { + this.remove(); + } else if (!state.needsArguments && expr.left === state.getArgumentsId() && t.isArrayExpression(expr.right)) { + return _.map(expr.right.elements, function (elem) { + return t.expressionStatement(elem); + }); + } + } +}; + +exports.Function = function (node, parent, scope) { // only tail recursion can be optimized as for now, // so we can skip anonymous functions entirely var ownerId = node.id; if (!ownerId) return; - var argumentsId, thisId, shouldContinueId, leftId; + var argumentsId, thisId, leftId, functionId, params, paramDecls; var state = { hasTailRecursion: false, + needsThis: false, + needsArguments: false, + setsArguments: false, ownerId: ownerId, + vars: [], getArgumentsId: function () { return argumentsId = argumentsId || scope.generateUidIdentifier("arguments"); @@ -170,37 +237,88 @@ exports.FunctionExpression = function (node, parent, scope) { return thisId = thisId || scope.generateUidIdentifier("this"); }, - getShouldContinueId: function () { - return shouldContinueId = shouldContinueId || scope.generateUidIdentifier("shouldContinue"); - }, - getLeftId: function () { return leftId = leftId || scope.generateUidIdentifier("left"); + }, + + getFunctionId: function () { + return functionId = functionId || scope.generateUidIdentifier("function"); + }, + + getParams: function () { + if (!params) { + params = node.params; + paramDecls = []; + for (var i = 0; i < params.length; i++) { + var param = params[i]; + if (!param._isDefaultPlaceholder) { + paramDecls.push(t.variableDeclarator( + param, + params[i] = scope.generateUidIdentifier("x") + )); + } + } + } + return params; } }; // traverse the function and look for tail recursion - scope.traverse(node, functionVisitor, state); + scope.traverse(node, firstPass, state); if (!state.hasTailRecursion) return; - var block = t.ensureBlock(node); + scope.traverse(node, secondPass, state); - if (leftId) { - block.body.unshift(t.variableDeclaration("var", [ - t.variableDeclarator(leftId) - ])); + if (!state.needsThis || !state.needsArguments) { + scope.traverse(node, thirdPass, state); } - var resultId = scope.generateUidIdentifier("result"); - state.getShouldContinueId(); + var body = t.ensureBlock(node).body; + + if (state.vars.length > 0) { + body.unshift(t.expressionStatement( + _(state.vars) + .map(function (decl) { + return decl.declarations; + }) + .flatten() + .reduceRight(function (expr, decl) { + return t.assignmentExpression("=", decl.id, expr); + }, t.identifier("undefined")) + )); + } + + if (paramDecls.length > 0) { + body.unshift(t.variableDeclaration("var", paramDecls)); + } node.body = util.template("tail-call-body", { - SHOULD_CONTINUE_ID: shouldContinueId, - ARGUMENTS_ID: argumentsId, - RESULT_ID: resultId, - CALLEE_ID: scope.generateUidIdentifier("callee"), - FUNCTION: t.functionExpression(null, node.params, block), - THIS_ID: thisId, + THIS_ID: thisId, + ARGUMENTS_ID: argumentsId, + FUNCTION_ID: state.getFunctionId(), + BLOCK: node.body }); + + var topVars = []; + + if (state.needsThis) { + topVars.push(t.variableDeclarator(state.getThisId(), t.thisExpression())); + } + + if (state.needsArguments || state.setsArguments) { + var decl = t.variableDeclarator(state.getArgumentsId()); + if (state.needsArguments) { + decl.init = t.identifier("arguments"); + } + topVars.push(decl); + } + + if (leftId) { + topVars.push(t.variableDeclarator(leftId)); + } + + if (topVars.length > 0) { + node.body.body.unshift(t.variableDeclaration("var", topVars)); + } }; diff --git a/lib/6to5/transformation/transformers/index.js b/lib/6to5/transformation/transformers/index.js index fea0e8ee5b..36829cda49 100644 --- a/lib/6to5/transformation/transformers/index.js +++ b/lib/6to5/transformation/transformers/index.js @@ -63,6 +63,8 @@ module.exports = { // needs to be after `es6.blockScoping` due to needing `letReferences` set on blocks "es6.blockScopingTDZ": require("./es6/block-scoping-tdz"), + // needs to be after `es6.parameters.*` and `es6.blockScoping` due to needing pure + // identifiers in parameters and variable declarators "es6.tailCall": require("./es6/tail-call"), regenerator: require("./other/regenerator"), diff --git a/lib/6to5/traversal/scope.js b/lib/6to5/traversal/scope.js index 286f460d4a..d155541b77 100644 --- a/lib/6to5/traversal/scope.js +++ b/lib/6to5/traversal/scope.js @@ -408,7 +408,7 @@ Scope.prototype.addBindingToFunctionScope = function (node, kind) { extend(scope.bindings, ids); extend(scope.references, ids); - if (kind) extend(scope.bindingKinds[kind], ids) + if (kind) extend(scope.bindingKinds[kind], ids); }; /** diff --git a/test/fixtures/transformation/es6-tail-call/call-apply/expected.js b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js index 240150ccdc..4708e26bdc 100755 --- a/test/fixtures/transformation/es6-tail-call/call-apply/expected.js +++ b/test/fixtures/transformation/es6-tail-call/call-apply/expected.js @@ -1,29 +1,20 @@ "use strict"; -(function f(n) { - var _arguments = arguments, - _this = this, - _shouldContinue, - _result; - var _callee = function (n) { +(function f(_x) { + var _this = this, + _arguments = arguments; + _function: while (true) { + var n = _x; if (n <= 0) { - console.log(this, arguments); + console.log(_this, _arguments); return "foo"; } if (Math.random() > 0.5) { - _arguments = [n - 1]; - _this = this; - return _shouldContinue = true; + _arguments = [_x = n - 1]; + continue _function; } else { - _arguments = [n - 1]; - _this = this; - return _shouldContinue = true; + _arguments = [_x = n - 1]; + continue _function; } - }; - - do { - _shouldContinue = false; - _result = _callee.apply(_this, _arguments); - } while (_shouldContinue); - return _result; -})(1000000) === "foo"; \ No newline at end of file + } +})(1000000) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/expressions/expected.js b/test/fixtures/transformation/es6-tail-call/expressions/expected.js index 0ac2e1625c..670ce95ae4 100755 --- a/test/fixtures/transformation/es6-tail-call/expressions/expected.js +++ b/test/fixtures/transformation/es6-tail-call/expressions/expected.js @@ -1,12 +1,9 @@ "use strict"; -(function f(n) { - var _arguments = arguments, - _this = this, - _shouldContinue, - _result; - var _callee = function (n) { - var _left; +(function f(_x) { + var _left; + _function: while (true) { + var n = _x; if (n <= 0) { return "foo"; } else { @@ -18,15 +15,8 @@ if (_left = getFalseValue()) { return _left; } - _arguments = [n - 1]; - _this = undefined; - return _shouldContinue = true; + _x = n - 1; + continue _function; } - }; - - do { - _shouldContinue = false; - _result = _callee.apply(_this, _arguments); - } while (_shouldContinue); - return _result; -})(1000000, true) === "foo"; \ No newline at end of file + } +})(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/factorial/actual.js b/test/fixtures/transformation/es6-tail-call/factorial/actual.js new file mode 100644 index 0000000000..6078e67fda --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/factorial/actual.js @@ -0,0 +1,3 @@ +function fact(n, acc = 1) { + return n > 1 ? fact(n - 1, acc * n) : acc; +} diff --git a/test/fixtures/transformation/es6-tail-call/factorial/expected.js b/test/fixtures/transformation/es6-tail-call/factorial/expected.js new file mode 100644 index 0000000000..5d53895e5b --- /dev/null +++ b/test/fixtures/transformation/es6-tail-call/factorial/expected.js @@ -0,0 +1,16 @@ +"use strict"; + +function fact(_x2) { + var _arguments = arguments; + _function: while (true) { + var n = _x2; + acc = undefined; + var acc = _arguments[1] === undefined ? 1 : _arguments[1]; + if (n > 1) { + _arguments = [_x2 = n - 1, acc * n]; + continue _function; + } else { + return acc; + } + } +} diff --git a/test/fixtures/transformation/es6-tail-call/recursion/actual.js b/test/fixtures/transformation/es6-tail-call/recursion/actual.js index 67de2e6943..5f13765dd6 100755 --- a/test/fixtures/transformation/es6-tail-call/recursion/actual.js +++ b/test/fixtures/transformation/es6-tail-call/recursion/actual.js @@ -1,8 +1,14 @@ -(function f(n = getDefaultValue(), /* should be undefined after first pass */ m) { +(function f(n, m = getDefaultValue()) { + // `m` should be `getDefaultValue()` after first pass if (n <= 0) { return "foo"; } - // Should be clean (undefined) on each pass - var local; + // `local1`-`local3` should be fresh on each pass + var local1; + let local2; + const local3 = 3; + // `g` should be function here on each pass + g = 123; + function g() {} return f(n - 1); })(1e6, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/recursion/expected.js b/test/fixtures/transformation/es6-tail-call/recursion/expected.js index a38d339867..c08a56c420 100755 --- a/test/fixtures/transformation/es6-tail-call/recursion/expected.js +++ b/test/fixtures/transformation/es6-tail-call/recursion/expected.js @@ -1,25 +1,24 @@ "use strict"; -(function f(_x, /* should be undefined after first pass */m) { - var _arguments = arguments, - _this = this, - _shouldContinue, - _result; - var _callee = function (_x, m) { - var n = arguments[0] === undefined ? getDefaultValue() : arguments[0]; +(function f(_x2) { + var _arguments = arguments; + _function: while (true) { + var g = function g() {}; + + var n = _x2; + m = local1 = local2 = local3 = undefined; + var m = _arguments[1] === undefined ? getDefaultValue() : _arguments[1]; + // `m` should be `getDefaultValue()` after first pass if (n <= 0) { return "foo"; } - // Should be clean (undefined) on each pass - var local; - _arguments = [n - 1]; - _this = undefined; - return _shouldContinue = true; - }; - - do { - _shouldContinue = false; - _result = _callee.apply(_this, _arguments); - } while (_shouldContinue); - return _result; -})(1000000, true) === "foo"; \ No newline at end of file + // `local1`-`local3` should be fresh on each pass + var local1; + var local2 = undefined; + var local3 = 3; + // `g` should be function here on each pass + g = 123; + _arguments = [_x2 = n - 1]; + continue _function; + } +})(1000000, true) === "foo"; diff --git a/test/fixtures/transformation/es6-tail-call/try-catch/expected.js b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js index fc4ac975cf..d1fb0c00e6 100755 --- a/test/fixtures/transformation/es6-tail-call/try-catch/expected.js +++ b/test/fixtures/transformation/es6-tail-call/try-catch/expected.js @@ -9,29 +9,19 @@ } catch (e) {} })(1000000) === "foo"; -(function f(n) { - var _arguments = arguments, - _this = this, - _shouldContinue, - _result; - var _callee = function (n) { +(function f(_x) { + _function: while (true) { + var n = _x; if (n <= 0) { return "foo"; } try { throw new Error(); } catch (e) { - _arguments = [n - 1]; - _this = undefined; - return _shouldContinue = true; + _x = n - 1; + continue _function; } - }; - - do { - _shouldContinue = false; - _result = _callee.apply(_this, _arguments); - } while (_shouldContinue); - return _result; + } })(1000000) === "foo"; (function f(n) { @@ -45,25 +35,15 @@ } finally {} })(1000000) === "foo"; -(function f(n) { - var _arguments = arguments, - _this = this, - _shouldContinue, - _result; - var _callee = function (n) { +(function f(_x) { + _function: while (true) { + var n = _x; if (n <= 0) { return "foo"; } try {} finally { - _arguments = [n - 1]; - _this = undefined; - return _shouldContinue = true; + _x = n - 1; + continue _function; } - }; - - do { - _shouldContinue = false; - _result = _callee.apply(_this, _arguments); - } while (_shouldContinue); - return _result; -})(1000000) === "foo"; \ No newline at end of file + } +})(1000000) === "foo";