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.

No comments:

Post a Comment