On Jul 23, 8:44 pm, "Richard Cornford wrote:
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 "empty" 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.