A recent book on JavaScript had a section on “Micro Optimizations” in to looping over arrays. In it they mentioned various ways to improve the speed of looping over the contents of an array. Modern JS virtual machines like V8 are optimized to the point that many of these optimizations are no longer valid.
In this post I’m going to explain the optimizations and test them in Node.js v0.8.22 and why you shouldn’t “Micro Optimize” your code.
So here we go…
// loop 1: simple loop
for (var c = 0; c < myArray.length; c++) {
myArray[c] = val;
}
// Loop 2: save array length in a variable
for (var c = 0, max = myArray.length; c < max; c++) {
myArray[c] = val;
}
// Loop 3: decrement counter
for (var c = myArray.length; c--;) {
myArray[c] = val;
}
Loop 1 has a counter variable that is incremented and compared to the length property of the array every time through the loop. If accessing the length property is expensive and the size of the array is very large, this can slow down your loop significantly.
Loop 2 optimization is to store the length property in a local variable to be used for the comparison. Now the counter can be compared to this local variable instead of accessing the length property of myArray.
Loop 3 optimization takes this one step further to eliminate the need for the comparison of two local variables. This saves a insignificant amount of memory in the grand scheme of things and makes the code a little shorter.
Here is the code for gathering metrics:
(function () {
'use strict';
// using a smaller array reduces the chance of the array data not being cached
var arraySize = 10000;
var numRuns = 100000;
console.log('Looping over ' + arraySize +
' elements in in a continuous array ' + numRuns + ' times.');
var date;
var totalTime1 = 0;
var totalTime2 = 0;
var totalTime3 = 0;
var array = getContinousArray(arraySize);
// run all the tests interweaved on the same array to minimize effects of
// caching
for (var i = 0; i < numRuns; i += 1) {
date = Date.now();
loop1(array, i * 2);
totalTime1 += Date.now() - date;
date = Date.now();
loop2(array, i * 3);
totalTime2 += Date.now() - date;
date = Date.now();
loop3(array, i * 5);
totalTime3 += Date.now() - date;
}
console.log('Loop 1 Avg Time: ' +
((totalTime1 / numRuns) * 1000).toFixed(3) + ' ms');
console.log('Loop 2 Avg Time: ' +
((totalTime2 / numRuns) * 1000).toFixed(3) + ' ms');
console.log('Loop 3 Avg Time: ' +
((totalTime3 / numRuns) * 1000).toFixed(3) + ' ms');
function loop1 (myArray, val) {
for (var c = 0; c < myArray.length; c++) {
myArray[c] = val;
}
}
function loop2 (myArray, val) {
for (var c = 0, max = myArray.length; c < max; c++) {
myArray[c] = val;
}
}
function loop3 (myArray, val) {
for (var c = myArray.length; c--;) {
myArray[c] = val;
}
}
// returns a new create a continuous array initialized to 0
// this should generate a continuous array in V8.
// we want to avoid accidentally creating sparce arrays...
function getContinousArray(arraySize) {
var cArray = [];
for (var i = 0; i < arraySize; i += 1) {
cArray.push(0);
}
return cArray;
}
}());
I ran these tests on a Mac Mini (4 core i7 2.6Ghz, 16GB RAM) running Mac OS 10.8.3 in Node.js v0.8.22 with the nice of -20. Here are the results:
% sudo nice -n -20 node micro-op.js && sudo nice -n -20 node micro-op.js && sudo nice -n -20 node micro-op.js && sudo nice -n -20 node micro-op.js
Looping over 10000 elements in in a continuous array 100000 times.
Loop 1 Avg Time: 11.660 ms
Loop 2 Avg Time: 12.180 ms
Loop 3 Avg Time: 14.440 ms
Looping over 10000 elements in in a continuous array 100000 times.
Loop 1 Avg Time: 12.140 ms
Loop 2 Avg Time: 11.840 ms
Loop 3 Avg Time: 14.420 ms
Looping over 10000 elements in in a continuous array 100000 times.
Loop 1 Avg Time: 11.850 ms
Loop 2 Avg Time: 11.680 ms
Loop 3 Avg Time: 14.750 ms
Looping over 10000 elements in in a continuous array 100000 times.
Loop 1 Avg Time: 11.500 ms
Loop 2 Avg Time: 11.880 ms
Loop 3 Avg Time: 14.980 ms
The loop 1 and loop 2 both performed roughly the same while loop 3 performed about 30% slower than either loop 1 or 2.
I find that many programers, myself included, will tend to optimize every single line of code. And even worse, these "optimizations" can actually perform worse than the more simple version.
Don't fall into the trap of optimizing your code based on the number of operations you think the it performs. Instead try to make your code verbose, simple, and most of all readable. Although this was a simple example in JavaScript, I think "Micro Optimizations" in any language can be detrimental. If you need to optimize, optimize where it counts (algorithms) and measure everything!!!
Am I wrong? Please comment below and post your tests!!!!