Wednesday, June 24, 2009

ES5 arguments and callee, I was wrong!

JavaScript is not JavaScript, I am not crazy, it is just a consideration between the language itself and what is behind it: another programming language with lower level rules and logic ... sounds silly and obvious, but please keep reading to understand what I mean.

No Results Yet, But I've Already Lost My Battle

I spread comments, I wrote post after posts to defend ECMAScript 5 arguments.callee decision with "use strict", but I have to admit I have never investigate the internal behavior of callee, an arguments property which is not what we think is ...

Discovering In Core The Callee Property

What I was thinking was something hilarious for a C or C++ programmer: an inherited property for a mutable instance.

// JavaScript should have a "secret" Arguments class
// and for each function, something like this
function Test(){};

// we have declared the function Test
// internally there should be a secret operation like this:

Test._createArguments = function(args){
for(var i = 0; i < args.length; i++)
this[i] = args[i];
};
Test._createArguments.prototype.callee = Test;


// and for each Test call, there should be
// a secret operation like this one:
Test._injectArguments(
new Test._createArguments(args);
);

// in order to retrieve Test arguments
// variable for each call ... inheriting
// automatically the callee property

Apparently, except for Internet Explorer, I was so wrong.
arguments.callee is not a pointer or a static variable retrieved instantly as {callee:"it's a me!"}.callee could be, arguments, plus callee is a property with a "discover latency" able to make each call execution up to 100 times slower than a regular function!

We Want Perfomance? Get Rid Of Arguments And Callee Then!

Even with most advanced JavaScript Engines, like the V8 one used by Google Chrome Browser, arguments and arguments.callee are a big bottleneck. Test by yourself!

(function(){
// Array to store execution time
var execution = [];

// named function
function factorial(i){
return i < 2 ? i : i * factorial(i - 1);
};
for(var i = 0, t = new Date; i < 1000; ++i)
factorial(100);
t = new Date - t;
execution.push(
"Native named function: " + t
);

// named function with arguments
function fact(i){
var i = arguments[0];
return i < 2 ? i : i * fact(i - 1);
};
for(var i = 0, t = new Date; i < 1000; ++i)
fact(100);
t = new Date - t;
execution.push(
"Native named with arguments: " + t
);

// arguments.callee
fact1 = function(i){
return i < 2 ? i : i * arguments.callee(i - 1);
};
for(var i = 0, t = new Date; i < 1000; ++i)
fact1(100);
t = new Date - t;
execution.push(
"arguments.callee: " + t
);

alert(execution.join("\n"));
})();

Impressive is the responsiveness of Internet Explorer, the browser which is "die hard" and the only one I was trying to defend about this decision, due to named functions misbehavior. IE is about 100 times slower when arguments variable is used, while Firefox 3 for example is about 50 time slower when arguments.callee is discovered. Chrome is about 4 times slower with both arguments, and callee, and the same is for Safari and Opera, impressive results. A little note about Chrome, apparently the main problem is the scope resolution, rather than arguments or callee.

We Still Love Scripting: A Callee Proposal

During these tests, I can proudly say I found a way to understand in which era we are, thanks to this simple snippet:

// WebReflection knows if we are in ES5 era!
navigator.ES5 = (function(){"use strict";try{return !arguments.callee}catch(e){return true}})();

Avoiding conflicts via global navigator object, ES5 will tell us if the browser is compatible with ECMAScript 5 "use strict"; rule or not, allowing our code to behave differently when necessary.
It does not really matter in any case, to use my callee proposal we need to inject a callee property, as I suggested in the latest post, and for the sake of good gode, via eval:

// Another WebReflection Silly Idea
function F/*or whatever name you prefer*/(callee){
return eval("(callee="+callee+")");
};

Above function is able to take a user defined function and to inject a callee property creating another defined function, a sort of operation usually performed once and never again. In few words, inside our function body we will find another variable with local scope: callee

(function(){
"use strict"; // or not

var execution = [];

// create the function via F
factorial = F(function(i){
// we got arguments, and callee
return i < 2 ? i : i * callee(i - 1);
});
for(var i = 0, t = new Date; i < 1000; ++i)
factorial(100);
t = new Date - t;
execution.push(
"WebReflection F: " + t
);

alert(execution.join("\n"));
})();

This strategy is particularly useful for these cases:

  • classic configuration object, new Ext.Panel({listeners:render:F(function(Panel){
    Panel.removeListener("render", callee);
    })})

  • classic pre-compiled function, var circle = F(Function("r,i","return !i ? r*callee(r," + Math.PI + ") : r*i"));

  • every time we would like to retrieve arguments.callee


Last point is the one that should let us think about what we have done so far.
If we try to execute latest benchmark, we will realize that specially with Internet Explorer, the evaluation trap works faster than arguments.callee so every time we need callee, but we do not need arguments, we should go for it.

Deeper Investigation

I'll keep digging inside this stuff but right now the only thing I can say is that I will try to avoid the usage of arguments.callee every single time I can because specially for the already JS speaking slow browser Internet Explorer, it is a performances killer I could not even imagine.

I hope I gave you more knowledge about this problem and why ECMAScript group decided to get rid of callee, and in a possible future get rid of arguments as well. One side is pro scriptish stuff, and we all love it, bot the other side is about how much this stuff could slow down Web possibilities. Stay tuned!

P.S. amazing, in FF and Chrome you can set whatever property into the navigator object and rather than transform it into a string a la window.name, you can safe whatever amount of data without loosing it until you do not close that tab!!! Stay tuned for this as well, it is a new security or hack problem we could use for better purposes ;)

4 comments:

Anonymous said...

So, if something is slow by design, instead of make it better, we remove it.. yeah, sure.

So if I have an house with old water pipes the solution is to remove the pipes...

Andrea Giammarchi said...

To be honest my last snippet purpose is to make callee better and faster but generally speaking if every browser is that slower when we use argumwnts or callee I bet they did their best to make it faster but there are obvious limts or overloads due arguments nature. Fair point in any case, who are you? :)

Nathan Toone said...

In ES5, then, is there still a way to get the calling function? That is, I currently use arguments.callee.caller quite often during debugging to see kind of a "mini" stack trace (combined with arguments.callee.caller.nom and firebug's console.count, it's nice for debugging *why* a function is getting called thousands of times)

It would be nice to have some sort of similar functionality still available...

Anonymous said...

https://mail.mozilla.org/pipermail/es-discuss/2009-March/008970.html

is a discussion of the issue.
the problem is:
var test = function(a,b,c){
someOtherFunction(arguments);
}

will allow the other function Write access to both test AND a,b,c.

also I would not use the eval solution you propose as that will not be fast either.

to work around it just name the function.
function workAround() {
var callee = workAround;
}