Re: addEvent - The late entry :)



On Jul 24, 2:46 am, kangax wrote:
On Jul 23, 8:44 pm, "Richard Cornford wrote:
<snip>
... . I did my comparisons against Prototype.js's - $A -
function , ...
<snip>
These were tests on a mixture of Pentium 4, Core 2 Duo and
Core 2 Quad processors and Windows and Mac OSs so the results
may not yet be sufficiently reprehensive for the comparisons
to hold in general. ...
<snip>
Thanks for an exhaustive comparison.

Didn't I make the point that they were not exhaustive clearly enough?

Unfortunately, prototype.js's $A is a general-purpose function.
That's one of the reasons why it's slower than other alternatives.
Besides converting arguments object into an array, it's often
used to convert array-like objects (namely NodeList's) into
actual arrays (by exploiting length property that NodeList's
expose).

What - $A - may or may not be is irrelevant to the question. The approach used in converting an - arguments - object into an array can be chosen at the point of needing to do it. There is no necessity to choose to use a general function to do it, and no reason for the changing of the approach used for converting - arguments - objects to arrays to impact on the - $A - function at all.

$A is also used for delegating behavior to a passed object's
toArray method if an object has one. Delegating allows to
decouple concerns, as we know, so this pattern allows other
data structures to define "toArray" containing its own logic.

As an example, String.prototype.toArray is defined as:

...
function() {
return this.split('');}

...

But we can be certain that - arguments - objects will not have - toArray - methods (unless they are explicitly assigned them).

It would probably be wiser to use a separate function when
converting arguments to an array, rather than relying on a
"heavier" $A. We might consider this in later revisions.

I did include a dedicated function in my tests (one that did little more than create a new array and loop over the 'array index' properties of its argument object copying values to corresponding properties of the new array). Inevitably it was a little faster than - $A - by virtue of doing less, but it was such a minimal difference that I would not regard it as worth consideration in comparison to the differences that some of the alternatives offer.

At this point it is probably worth posting my test code. The following is a simple scripted HTML page. At the top there is a filed for entering the number of iterations to perform (which may need adjusting for particular browser/OS/hardware combinations). Generally, the number of iterations should be the most that any particular system will allow without putting up the 'A script on this page ... " dialog, and certainly large enough to make the total duration of _all_ the results greater than 300 milliseconds at (absolute) minimum (as the 10 millisecond resolution of most browser timers would make shorter durations close to meaningless).

There is a table at the bottom of the page into which the results are written, and a number of buttons ladled 'Test' to start to test process (which are disabled while the test runs). On of these buttons is under the output table at the bottom of the page. Individual loop runs are separated with - setTimeout - calls with long-ish intervals (to guarantee that the browser has time to catch up with any work it needs to do (including re-laying out the results table) and reduce the likelihood of seeing the "A Script on this page ..." dialog.

The individual test functions are stored in an array of objects with the global identifier - fncts -. These object have - description - and - testFnc - properties, where - testFnc - is the function that executes the expression being tested. With the exception of the first item in this array (which is used to gauge the overheads involved in the test loop) items in this array may be removed, or alternatives added. Layout and overall test looping is driven from the length of this array. The last item in the array is the one against which comparisons are made and should be the one expected to be slowest.

And array of argument sets with the global identifier - args - is used to demonstrate the impact of changing numbers of arguments on the process. The number of entries in this array can also be changed as the array length is used to drive that aspect of the test process. Also, currently all of the arguments are numeric literals. Other types of arguments may be tried to see whether that has an impact on performance (unlikely but not impossible).

<html>
<head>
<title>arguemnts to Array tests</title>
<style type="text/css">
TD.alRight {
text-align:right;
}
</style>
<script type="text/javascript">


function $A(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
var length = iterable.length || 0, results = new Array(length);
while (length--) results[length] = iterable[length];
return results;
}
function makeArray(iterable) {
var c, ar = [];
if((c = iterable.length)){
do{
ar[--c] = iterable[c];
}while(c)
}
return ar;
}

var args = [
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],
[1,2,3,4,5,6,7,8,9,10],
[1,2,3,4,5,6,7,8,9],
[1,2,3,4,5,6,7,8],
[1,2,3,4,5,6,7],
[1,2,3,4,5,6],
[1,2,3,4,5],
[1,2,3,4],
[1,2,3],
[1,2],
[1],
[]
];

var argsIndex = 0;

var frm = null;
var fncts = [
/* Timing of an "empty" loop to estimate the overheads of the
test itself. */
{
testFnc:function(){
return arguments;
},
description:'Only overheads.'
},
{
testFnc:function(){
var ar = [];
ar.push.apply(ar, arguments);
return ar;
},
description:'push.apply'
},
{
testFnc:function(){
var ar = [];
ar.unshift.apply(ar, arguments);
return ar;
},
description:'unshift.apply'
},
{
testFnc:function(){
return (
arguments.length == 1)?
[arguments[0]]:
Array.apply(null, arguments
);
},
description:'Array.apply(null'
},
{
testFnc:function(){
return (
arguments.length == 1)?
[arguments[0]]:
Array.apply(this, arguments
);
},
description:'Array.apply(this'
},
{
testFnc:function(){
return Array.prototype.slice.call(arguments, 0);
},
description:'slice.call'
},
{
testFnc:function(){
return Array.prototype.splice.call(
arguments, 0, arguments.length
);
},
description:'splice.call'
},
{
testFnc:function(){
switch(arguments.length){
case 0:
return [];
case 1:
return [arguments[0]];
default:
return Array.apply(this, arguments);
}
},
description:'switch'
},
{
testFnc:function(){
return makeArray(arguments);
},
description:'makeArray'
},
{
testFnc:function(){
return $A(arguments);
},
description:'$A'
}
];

function runTest(p){
var lim = +frm['loopLimit'].value;
var N, totTime, stTime;
var obj = {};
stTime = new Date().getTime();
for(var c = 0;c < lim;c++){
N = fncts[p].testFnc.apply(this, args[argsIndex]);
}
totTime = (new Date().getTime() - stTime);
frm["Dur"+p].value = totTime;
frm["Avr"+p].value = (totTime/lim);
frm["Res"+p].value = N;
act(p+1);
}

var f;
var running = false;
function setButtons(bl){
frm['loopLimit'].disabled = bl;
var sw = frm['bt'];
if(typeof sw.length == 'undefined'){
sw = [sw];
}
for(var c = 0;c < sw.length;c++){
sw[c].disabled = bl;
}
}
function startTests(){
if(!running){
frm = document.forms['f'].elements;
setButtons(true);
frm["Dur0"].value = '';frm["Avr0"].value = '';
for(var c = 1;c < fncts.length;c++){
frm["Dur"+c].value = '';
frm["Avr"+c].value = '';
frm["Res"+c].value = '';
}
running = true;
act(0);
}
}
function act(p){
/* setTimeout is used to minimise the occurrences
of 'a script on this page is running slow' dialogs. */
if(p >= fncts.length){
++argsIndex;
if(argsIndex < args.length){
setTimeout('report();startTests()',1000);
}else{
setTimeout('report();argsIndex = 0;',1000);
}
}else{
setTimeout(('(f = runTest('+p+'));'),2000);
}
}
function report(){
var co = ((argsIndex - 1)<<1)+1
var emDur, unaC, diff1, diff2, c, evaC;
var lim = +frm['loopLimit'].value;
var row, tBody = document.getElementById('outBody');
emDur = +frm["Dur0"].value;
c = (fncts.length-1);
row = tBody.rows[1+c];
diff1 = (frm["Dur"+c].value - emDur); //if this is negative then
//the whole test is invalid.
if(diff1 < 0){
// "Empty" loop longer than base test loop
row.cells[co].innerHTML = '+++++++'
return;
}
unaC = diff1 / lim;
if(!unaC){
row.cells[co].innerHTML = 'Loop too short';
return;
}
row.cells[co].innerHTML = formatVal(100);
row.cells[co+1].innerHTML = formatVal(unaC);
for(c = 1;c < (fncts.length-1);c++){
row = tBody.rows[1+c];
diff2 = (frm["Dur"+c].value - emDur);
if(diff2 < 0){
//"Empty" loop longer than this test';
row.cells[co].innerHTML = '*******'
}else{
evaC = diff2 / lim;
row.cells[co].innerHTML = formatVal(((evaC/unaC)*100));
row.cells[co+1].innerHTML = formatVal(evaC);
}
}
tBody.rows[0].cells[0].innerHTML = (navigator.userAgent);
tBody.rows[1].cells[0].innerHTML = ('Iterations = '+lim);
setButtons(false);
running = false;
}
function roundToR(X, R) { return Math.round(X*R)/R }
function formatVal(n){
if(isNaN(n)){
return '------'
}
var s = String(roundToR(n, 10000));
var ind;
if((ind = s.indexOf('.')) < 0){
s += '.';
ind = s.length - 1;
}
s += '000'
return s.substring(0, (ind+4));
}
</script>
</head>
<body>
<div>
<form name="f" action="#">
Loop Length = <input type="text" value="80000"
name="loopLimit"><br><br>


<input type="button" value="Test" name="bt" onclick="startTests();">
Repeat tests to reduce/expose the influence of background tasks.
<br><br>
Empty Loop Duration (milliseconds) = <input type="text" value="X"
name="Dur0"><br>
Empty Loop Average (milliseconds) = <input type="text" value="X"
name="Avr0" size="22"><br>
(result = <input type="text" value="X" name="Res0" size="42">)<br>
<br>

<script type="text/javascript">
var c, len = fncts.length;
var st = ''
for(c = 1;c < len;++c){
st += '<br><code>'+fncts[c].description;
st += '</code> Duration (milliseconds) = ';
st += '<input type="text" value="X" name="Dur'+c+'"><br>';
st += '<code>'+fncts[c].description;
st += '</code> Average (milliseconds) = ';
st += '<input type="text" value="X" name="Avr'+c;
st += '" size="22"><br>';
st += '(result = <input type="text" value="X" name="Res'+c;
st += '" size="42">)<br>';
}
document.write(st);
</script>

<input type="button" value="Test" name="bt" onclick="startTests();">
Repeat tests to reduce/expose the influence of background tasks.
<br><br>

Average: (duration of test - duration of &quot;empty&quot; loop)
/ loop length (Milliseconds)<br>

<script type="text/javascript">
var tdsOut = [''];
tdsOut.toString = function(){
return ('<td>'+this.join('<\/td><td class="alRight">')+'<\/td>');
}
var rowsOut = [];
rowsOut.toString = function(){
return ('<tr>'+this.join('<\/tr><tr>')+'<\/tr>');
}
var tableOut = [
'<table border="2"><tbody id="outBody">',
rowsOut,
'<\/tbody><\/table>'
]

//fncts.length // vertical
//args.length){ //horezontal
var c, d, len = fncts.length, dLen = args.length;
var st = '<th><\/th>'
for(d = 0;d < dLen;++d){
st += '<th colspan="2">N<sup>o</sup> args = ';
st += args[d].length+'<\/th>'
}
rowsOut.push(st);
for(d = 0;d < dLen;++d){
tdsOut.push('%');
tdsOut.push('Average');
}
rowsOut.push(String(tdsOut));
for(c = 1;c < len;++c){
tdsOut.length = 0;
tdsOut.push(c+':'+fncts[c].description);
for(d = 0;d < dLen;++d){
tdsOut.push('');
tdsOut.push('');
}
rowsOut.push(String(tdsOut));
}
document.write(tableOut.join(''));
</script>
<br><br>
<input type="button" value="Test" name="bt" onclick="startTests();">
Repeat tests to reduce/expose the influence of background tasks.
<br><br>
</form>
</div>
</body>
</html>

It may be observed that the process with the description - switch - is actually the fasted across the board. Suggesting that short-circuiting the process in the (probably common) case of having zero arguments is beneficial . However, I have not proposed this as the fasted approach because I was interested in a single expression that could replace the - $A(arguments) - expression, and - switch - is a statement not an expression. Moving the switch statement into a function and calling that in place of - $A - would add the function call overheads and so may negate its advantages (indeed a quick test of that in IE6 shows the function call overheads have that approach drop to 120% of - $A - performance at two arguments and it does not overtake - $A - again until the number of arguments gets up to 5).

If the zero arguments case really is expected to be common then the expression:-

(
(!arguments.length)?
[]:
(
(arguments.length == 1)?
[arguments[0]]:
Array.apply(this, arguments)
)
)

- might prove advantageous (performance-wise, otherwise it is getting too big/complex). Though it would still be possible to recognise the common case at the point of calling and so call a faster alternative instead and so avoid any need to be processing arguments object for that case.

Richard.

.