programming JavaScript Dec 17, 2015

Making Babel fast with ES2015 rest parameters

This post is a follow-up of JavaScript performance with Babel and Node.js: a case against default parameters in tail call optimizations (opens new window). At the time, Babel 6 had only been published for a few hours.

When Babel 6 was released, I quickly realised that I kind of missed my target: tail call optimisation had been dropped in the process. But all was not lost, I could still investigate Babel's use of arguments.

Fixture tests

First, I looked at a lot of fixture tests. These are files meant to test if a particular Babel transform or plug-in works properly. They consist of two files: actual.js (ES2015 code) and expected.js. The goal of this test is to check if the output of babel actual.js matches the content of expected.js.

I noticed something about a particular transform : babel-plugin-transform-es2015-parameters, more precisely about its handling of rest parameters:

actual.jsvar concat = (...arrs) => {
  var x = arrs[0];
  var y = arrs[1];
};
1
2
3
4
expected.jsvar concat = function () {
  var x = arguments[0];
  var y = arguments[1];
};
1
2
3
4

This is unsafe. V8 will only be able to optimise the concat function if the arguments object has a length greater than 1. Otherwise, for example concat([0]), the attempt to access the undefined arguments[1] will force V8 to rematerialize arguments on the fly, preventing the whole function from being optimised.

First attempt

Having no idea about Babel's codebase and internals, it took me a whole weekend to come up with a first patch: #2833: Have es2015 rest transform safely use arguments (opens new window). It fixed some of the rest-transformed unsafe use of arguments and it got merged after five weeks (which is way too long by the way, but I don't blame anyone, I'm pretty sure it was an exceptional situation where someone said they would take care of this PR, then got busy, and in the meantime nobody saw the need to take over because someone was already in charge. No big deal).

At first I was pretty happy with this patch. The new expected.js looked like this:

expected.jsvar concat = function () {
  var x = arguments.length <= 0 || arguments[0] === undefined ? undefined : arguments[0];
  var y = arguments.length <= 1 || arguments[1] === undefined ? undefined : arguments[1];
};
1
2
3
4

which was safe. Some basic benchmarks were showing a 4x speedup. The tests were green. I had learned a lot about how Babel works.

Until someone noticed the pattern I was using was a bit weird (opens new window). In fact, the reason I initially chose this pattern was that I got it from here (opens new window). While it makes sense to use it for default parameters handling (ARGUMENTS.length <= INDEX || ARGUMENTS[INDEX] === undefined ? DEFAULT_VALUE : ARGUMENTS[INDEX];), it becomes overly complicated in the case where DEFAULT_VALUE is undefined.

Second attempt

I was fixing this pattern issue, replacing it with ARGUMENTS.length <= INDEX ? undefined : ARGUMENTS[INDEX], when I noticed my previous patch was incomplete.

actual.jsvar t = function (f, ...items) {
    var x = f;
    x = items[0];
    x = items[1];
};
1
2
3
4
5

was still being converted to:

expected.jsvar t = function (f) {
    var x = f;
    x = arguments[1];
    x = arguments[2];
};
1
2
3
4
5

The transform was not taking into account the presence of a rest parameter when there were other parameters involved (function (f, ...items)). After I fixed this issue, I had another one: x = items[1] was correctly transformed, but not x[1] = ..., x.p = ... or ... = items[1] || something. I had to generalise the patch to (safely) cover all possible occurrences of accessing a value from a rest argument.

I added a fixture test, reworked my patch and opened a new PR: Fixing T6818 (opens new window).

actual.jsfunction u(f, g, ...items) {
    var x = f;
    var y = g;
    x[12] = items[0];
    y.prop = items[1];
    var z = items[2] | 0 || 12;
}
1
2
3
4
5
6
7
expected.jsfunction u(f, g) {
    var x = f;
    var y = g;
    x[12] = arguments.length <= 2 ? undefined : arguments[2];
    y.prop = arguments.length <= 3 ? undefined : arguments[3];
    var z = (arguments.length <= 4 ? undefined : arguments[4]) | 0 || 12;
}
1
2
3
4
5
6
7

Hopefully, this part is done. I'll try to find some other Crankshaft-related-JS-anti-patterns in what Babel generates.

#javascript #es2015 #babel #nodejs #crankshaft #v8

Follow me on Twitter, GitHub.

unless otherwise stated, all content & design © copyright 2021 victor felder