Hello, I have been researching Javascript performance. I've learned that when accessing more than once, it is usually best to copy closure variables and class members into local scope to speed things up. For example:
var i = 100;
var doSomething = function () {
var localI = i;
// do something with localI a bunch of times
var obj = {
a: 100
};
var objA = obj.a;
// do something with objA a bunch of times
};
I understand this; it adds a shortcut for the interpreter looking up the value by name. This concept becomes very unclear when dealing with methods. At first, I thought it would work the same way. For example:
var obj = {
fn: function () {
// Do something
return this.value;
},
value: 100
};
var objFn = obj.fn
objFn();
// call objFn a bunch of times
As it is, this will not work at all. Accessing the method like this removes it from its scope. When it reaches the line this.value, this refers to the window object and this.value will probably be undefined. Instead of directly calling objFn and loosing scope, I could pass its scope back into it with objFn.call(obj) but does this perform any better or worse then the original obj.fn()?
I decided to write a script to test this and I got very confusing results. This script makes iterations over several tests which loop through the above function calls many times. The average time taken for each test is output to the body.
An object is created with many simple methods on it. The extra methods are there to determine if the interpreter has to work much harder to locate a specific method.
Test 1 simply calls this.a();
Test 2 creates a local variable a = this.a then calls a.call(this);
Test 3 creates a local variable using YUI's bind function to preserve scope. I commented this out. The extra function calls created by YUI make this way slower.
Tests 4, 5, and 6 are copies of 1, 2, 3 except using z instead of a.
YUI's later function is used to prevent runaway script errors. The timing is done in the actual test methods so the setTimeouts should not effect the results. Each function is called a total of 10000000 times. (Easily configurable if you want to run tests.)
Here's my entire XHTML document I used to test.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xml:lang="en" dir="ltr">
<head>
<script type="text/javascript" src="http://yui.yahooapis.com/combo?3.1.2/build/yui/yui-min.js"></script>
<script>
YUI().use('node', function (Y) {
var o = {
value: '',
a: function () {
this.value += 'a';
},
b: function () {
this.value += 'b';
},
c: function () {
this.value += 'c';
},
d: function () {
this.value += 'd';
},
e: function () {
this.value += 'e';
},
f: function () {
this.value += 'f';
},
g: function () {
this.value += 'g';
},
h: function () {
this.value += 'h';
},
i: function () {
this.value += 'i';
},
j: function () {
this.value += 'j';
},
k: function () {
this.value += 'k';
},
l: function () {
this.value += 'l';
},
m: function () {
this.value += 'm';
},
n: function () {
this.value += 'n';
},
o: function () {
this.value += 'o';
},
p: function () {
this.value += 'p';
},
q: function () {
this.value += 'q';
},
r: function () {
this.value += 'r';
},
s: function () {
this.value += 's';
},
t: function () {
this.value += 't';
},
u: function () {
this.value += 'u';
},
v: function () {
this.value += 'v';
},
w: function () {
this.value += 'w';
},
x: function () {
this.value += 'x';
},
y: function () {
this.value += 'y';
},
z: function () {
this.value += 'z';
},
reset: function () {
this.value = '';
},
test1: function (length) {
var time = new Date().getTime();
while ((length -= 1)) {
this.a();
}
return new Date().getTime() - time;
},
test2: function (length) {
var a = this.a,
time = new Date().getTime();
while ((length -= 1)) {
a.call(this);
}
return new Date().getTime() - time;
},
test3: function (length) {
var a = Y.bind(this.a, this),
time = new Date().getTime();
while ((length -= 1)) {
a();
}
return new Date().getTime() - time;
},
test4: function (length) {
var time = new Date().getTime();
while ((length -= 1)) {
this.z();
}
return new Date().getTime() - time;
},
test5: function (length) {
var z = this.z,
time = new Date().getTime();
while ((length -= 1)) {
z.call(this);
}
return new Date().getTime() - time;
},
test6: function (length) {
var z = Y.bind(this.z, this),
time = new Date().getTime();
while ((length -= 1)) {
z();
}
return new Date().getTime() - time;
}
},
iterations = 100, iteration = iterations, length = 100000,
t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, body = Y.one('body');
body.set('innerHTML', '<span>Running ' + iterations + ' Iterations…</span>');
while ((iteration -= 1)) {
Y.later(1, null, function (iteration) {
Y.later(1, null, function () {
o.reset();
t1 += o.test1(length);
});
Y.later(1, null, function () {
o.reset();
t2 += o.test2(length);
});
/*Y.later(1, null, function () {
o.reset();
t3 += o.test3(length);
});*/
Y.later(1, null, function () {
o.reset();
t4 += o.test4(length);
});
Y.later(1, null, function () {
o.reset();
t5 += o.test5(length);
});
/*Y.later(1, null, function () {
o.reset();
t6 += o.test6(length);
});*/
if (iteration === 1) {
Y.later(10, null, function () {
t1 /= iterations;
t2 /= iterations;
//t3 /= iterations;
t4 /= iterations;
t5 /= iterations;
//t6 /= iterations;
//body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 3: a();</dt><dd>' + t3 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd><dt>Test 6: z();</dt><dd>' + t6 + '</dd></dl>');
body.set('innerHTML', '<dl><dt>Test 1: this.a();</dt><dd>' + t1 + '</dd><dt>Test 2: a.call(this);</dt><dd>' + t2 + '</dd><dt>Test 4: this.z();</dt><dd>' + t4 + '</dd><dt>Test 5: z.call(this);</dt><dd>' + t5 + '</dd></dl>');
});
}
}, iteration);
}
});
</script>
</head>
<body>
</body>
</html>
I've run this using Windows 7 in three different browsers. These results are in milliseconds.
Firefox 3.6.8
Test 1: this.a();
9.23
Test 2: a.call(this);
9.67
Test 4: this.z();
9.2
Test 5: z.call(this);
9.61
Chrome 7.0.503.0
Test 1: this.a();
5.25
Test 2: a.call(this);
4.66
Test 4: this.z();
3.71
Test 5: z.call(this);
4.15
Internet Explorer 8
Test 1: this.a();
168.2
Test 2: a.call(this);
197.94
Test 4: this.z();
169.6
Test 5: z.call(this);
199.02
Firefox and Internet Explorer produced results about how I expected. Test 1 and Test 4 are relatively close, Test 2 and Test 5 are relatively close, and Test 2 and Test 5 take longer than Test 1 and Test 4 because there is an extra function call to process.
Chrome I don't understand at all, but it's so much faster, perhaps the tweaking of sub millisecond performance is unnecessary.
Does anyone have a good explanation of the results? What is the best way to call Javascript methods multiple times?