My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Saturday, February 13, 2010

arguments, callee, call, and apply performances

We have dozens of best practices to improve performances. We also have common practices to accomplish daily tasks. This post is about the most used JavaScript ArrayLike Object, aka arguments, and its performances impact over basic tasks.

Why arguments

While it's natural for JavaScripters to use such "magic" variable, as arguments is, in many other languages everybody knows it does not come for free and it is rarely used.

<?php
function myFunc() {
// function call for each execution
// rarely seen in good PHP scripts
$arguments = func_get_args();
}
?>

One clear advantage in PHP, Python, and many others, is the possibility to define a default value for each argument.

<?php

class UserManager extends MyDAL {
public function exists($user='unknown', $pass='') {
return $this->fetch('SELECT 1 FROM table WHERE user=? AND pass=?', $user, $pass);
}
}
?>

This approach may brings automatically developers to code as if arguments does not exist. It's not an important value, everything has a default ... so, why bother?

Why callee

Specially because of black sheep Internet Explorer and its inconsistent/bugged (it does not exist, imho) concept of named function expression, our laziness frequently bring us to use this "shortcut" to refer the current executed function.

// the classic case ..
setTimeout(function
/*
if named, IE will pollute the current scope
with chose name rather than let it "be" only
inside the function body, as is for every other browser
*/
() {
// do stuff
setTimeout(arguments.callee, 1000);
}, 1000);

Even if we are in a closure so that no risk will occur if we name the callback, we are still lazy and we don't even think about something like:

setTimeout(function $_fn1() {
// do stuff
setTimeout($_fn1, 1000);
}, 1000);

return function $_fn2() {
// if stuff $_fn2() again;
};

Of course, how many $_fn1, $_fn2, $_fn3 we can possibly have in the current scope?
Somebody could even think about silly solutions such:

Function.prototype.withCallee = (function ($) {
// WebReflection Silly Ideas - Mit Style
var toString = $.prototype.toString;
return function withCallee() {
return $("var callee=" + toString.call(this) + ";return callee")();
};
})(Function);

// runtime factorial
alert(function(n){
return n * (1 < n ? callee(--n) : 1);
}.withCallee()(5)); // 120

// other example
setTimeout(function () {
// bit slower creation ... but
// much faster execution for each successive call
if (animationStuff) {
setTimeout(callee, 15);
}
}.withCallee(), 15);


OK, agreed that 2 functions rather than one for each function that would like to use callee could require a bit more memory consumption ... but hey, we are talking about performances, right?

Why call and apply

Well, call and apply are one of the best JavaScript part ever. Everything can be injected into another scope, referenced via this, and while Python, as example, has a clear self as first argument, we, as JavaScripters, don't even think about such solution: we've got call and apply, who needs to optimize a this?
Well, somehow this always remind us that we are dealing with an instance, an object, rather than a primitive or whatever value sent as argument.
This means that even where it is possible to avoid it, we feel cooler using such mechanism:

function A(){};
A.prototype = (function () {
// our private closure to have private methods
function _doStuff() {
this.stuff = "done";
}
return {
constructor:A,
doStuff:function () {
_doStuff.call(this);

// it could have been a simple
_doStuff(this);
// if _doStuff was accepting a self argument
}
};
})();

Furthermore, apply is able to combine both worlds, via lazy arguments discovery, and context injection ... how cool it is ...

The Benchmark

Since we have all these approaches to solve our daily tasks, and since these cannot come for free, I have decided to create a truly simple bench, hopefully compatible with a wide range of browsers. There is nothing there, except lots of executions, defined by times parameter in the query string, and a simple table to compare runtime these results.

Interesting Results

The scenario is apparently totally inconsistent across all major browsers, and this is my personal summary, you can deduct your one as well:
  • in IE call and apply are up to 1.5X slower while as soon as arguments is discovered, we have up to 4X performances gap. There is no consistent difference if we discover callee, since it seems to be attached directly into arguments object.
  • in Chrome call is slower than apply only if there are no arguments sent, otherwise call is 4X faster than apply and, apparently, even faster than a direct call. arguments costs generally up to 2.5X while once discovered, callee seems to come for free giving sometimes similar direct call results.
  • in Firefox things are completely different again. Direct call, as call and apply, do not differ that much but as soon as we try to discover arguments.callee, for one of the first browser that got named function expression right, the execution speed is up to 9X slower.
  • Opera seems to be the most linear one. Direct call is faster than call, and call is faster than callee. To discover arguments we slow down up to 2X while callee does not mean much more.
  • In Safari we have again a linear increment, but callee costs more than Opera and others, surely not that much as is for Firefox


Summarized Results

A direct call is faster, cross browser speaking, and specially for those shared functions without arguments, we could avoid usage of call or apply, a self reference as argument is more than enough.
arguments object should be forbidden, if we talk about extreme performances optimizations. This is the only real constant in the whole bench, as soon as it is present, it
slows down every single function call and most of the time
consistently.

HINTS about arguments

To understand if an argument has not been sent, we can always use this check ignoring JSLint warnings about it:

function A(a, b, c) {
if (c == null) {
// c can be ONLY undefined OR null
}
}

If we compare whatever value with == null, rather than === null, we can be sure this value is null or undefined.
Since generally speaking undefined is not an interesting value and null is used instead, also because undefined is a variable and it costs to compare something against it and it could be redefined as well while null cannot, it does not make sense at all to do tedious checks like this:

function A(a, b, c) {
// JSLint way ...
if (c === undefined || c === null) {
// bye bye performances
// bye bye security, undefined can be reassigned
// hello redundant code, == null does exactly the same
// check in a more secure way since it does not matter
// if undefined has been redefined
}
}

Do we agree? That warning in JSLint is one of the most annoying one, at least this is my opinion.
Let's move forward.
If we would like to know arguments length we have different strategies:

function Ajax(method, uri, async, user, pass) {
if (user == null) {
// we know pass won't be there as well
// received probably 3 arguments
// if user is not null, we expect 5 arguments
// and we use all of them
}
if (async == null) {
// this is a sync call
// received 2 arguments
}
}

function count(a, b, c, d) {
// not null, we consider it as a valid value
var argsLength = (a != null) + (b != null) + (c != null) + (d != null);
alert(argsLength);
// rather than alert(arguments.length);
}

count();
count(1);
count(1, 2);
count(1, 2, 3);
count(1, 2, 3, 4);

About latest suggestion please consider that only Chrome is slower, but Chrome is already the fastest browser so far while in IE, as example, arguments.length rather than null checks costs up to 6X the time.
Every other browser will have better performances than arguments.length, then we need to test case after case since a function, as String.fromCharCode could be, cannot obviously use such strategy due to "infinite" accepted arguments.
In these cases, e.g. runtime push or similar methods, we don't have many options ... but these should be exceptions, not the common approach, as is for many other programming languages with some arguments support.

Conclusion

I do not pretend to change developers code style with a single post and things are definitively not that easy to normalize for each browser.
Unfortunately, we cannot even think about features detection when we talk about performances, we don't want 1 second delay to test all performances cases before we can decide which strategy will speed up more, do we?
At least we are now better aware about how much these common JavaScript practices could slow down our code on daily basis and, when performances do matter, we have basis to avoid some micro bottleneck.

5 comments:

qFox said...

It's kind of weird to see browsers having so much slowdown when arguments is used (almost makes you happy they optimize for it). All the spec says is make an image map for it.

In your tests, did you also take into account the overhead compared to the function body?

Like, having a function which simply "mentions" arguments and nothing else might give you a slowdown, but when the body of the function contains other code, how does the arguments slowdown compare to that?

In other words, you have to put this into a certain perspective... Does avoiding "arguments" and jumping through hoops to do so outweigh the speed supposedly gained and readability of the resulting code?

Andrea Giammarchi said...

qFox I am not sure I have got your point ... I am testing the difference between bodies with arguments, and bodies without.
To make the usage of arguments not ignorable I push results into a stack (that will be empty after each bench).
This is already another operation inside the function body, and we all can see the difference.
If an execution takes 1 second, of course "0.1" ms are not important.
The problem is that whatever profiler we use, we can easily spot each JavaScript session has thousands of hundreds of function calls, and in this case, the total amount of added milliseconds counts.

Firefox seems to be the only one not affected at all about arguments presence, but check IE out, and other as well :(

Samer Ziadeh said...

What if you pass in an object as an argument would that make it faster? So instead of passing in 5 arguments to the function you pass in 1 object with 5 properties.

Andrea Giammarchi said...

Object creation via literals is truly fast in JavaScript. A shared argument object.
Via object it is possible to provide multiple arguments without effort, or enrich the object itself.
At the end of the day, technically speaking, there is no difference between an object and N arguments, almost same performances but as soon as we discover "arguments", it does not matter if we passed one argument rather than more, it is the arguments magic variable itself the problem (and if you talk with engines core guy they can confirm ideally they would get rid of arguments object).

Anonymous said...

I've looking for this info for a long time, thanks for your work.