Benchmark: Array-ification of arguments

Having really hot paths in your code can be great, but it can also be a little dangerous.

When v8’s Crankshaft landed in the Node.js dev build, it immediately resulted in a significant drop in Node’s “hello world” performance benchmark.

This was alarming. Crankshaft was supposed to be faster, and here it was causing slowdowns.

The cause was tracked down Vyacheslav Egorov (known to many in the Node community as “mraleph”), and had to do with Node’s use of the arguments object in the EventEmitter.prototype.emit function.

Since this function is the hottest code in Node, even a slight decrease in performance is immediately felt. Vyacheslav discussed the commit in a google plus post.

This got me thinking about the best ways to convert the arguments object, or part of it, to a standard JavaScript Array. I pulled out the node benchmarking thing I wrote, and wrote up two tests

Disclaimers

This advice is only relevant in the latest v8. By the time you read it, it’s already out of date and incorrect. Just stop now, run your own tests, and make your own choices.

If node.js is still using v8 version 3.4.12.1, then it might be just a little valid for your node programs, but otherwise, no. Stop. Don’t even continue reading.

Results

First, if your function takes a variable number of arguments, and you don’t know how many it will be called with do not define any named parameters. It makes things more complicated, and dramatically slows down the process.

Want to just put all the arguments in an array? Do this:

function varArgsList () {
  var args = arguments.length === 1
           ? [arguments[0]] : Array.apply(null, arguments)

It’s an order of magnitude faster than Array.prototype.slice.call or [].slice.call.

Want to get all the arguments in a particular slice? (Like, say, everything after the first, or the second until the second-to-last, etc.) Well, that’s a bit more complicated. There, as with the patch from Mr. Aleph, you’ve gotta walk the list yourself.

For example, to do the equivalent of Array.prototype.slice.call(arguments, 1), pulling off every argument after the first, the fastest method seems to be this:

function manualMap () {
  var l = arguments.length
  var arr = new Array(l - 1)
  for (var i = 1; i < l; i ++) arr[i - 1] = arguments[i]

The fastest, if you know exactly which arguments are which, is to refer to them as arguments[i], and only array-ify if and when it’s absolutely necessary. So, not surprisingly, I was unable to out-perform Mr. Aleph in a v8 benchmark-off. My goal in this was simply to figure out how much of a difference it makes, and write a script to check it later, in case I find myself in a similar situation again.

Of course, [].slice.call(arguments, 1, 2) is far fewer bytes, and easier to know what’s going on. For most programs, the slight difference is not going to affect your overall program. But sometimes hot code is hot enough to slow your benchmarks down by 20% because of what seems like a trivial change.

It’s only over-optimizing if it doesn’t make a significant difference.