四.Algorithms and Flow Control
The overall structure of your code is one of the main determinants as to how fast it will execute. Having a very small amount of code doesn’t necessarily mean that it will run quickly, and having a large amount of code doesn’t necessarily mean that it will run slowly. A lot of the performance impact is directly related to how the code has been organized and how you’re attempting to solve a given problem.
The techniques in this chapter aren’t necessarily unique to JavaScript and are often taught as performance optimizations for other languages. There are some deviations from advice given for other languages, though, as there are many more JavaScript engines to deal with and their quirks need to be considered, but all of the techniques are based on prevailing computer science knowledge.
for (var i=0; i < 10; i++){ //loop body }The for loop tends to be the most commonly used JavaScript looping construct. There are four parts to the for loop: initialization, pretest condition, post-execute, and the loop body. When a for loop is encountered, the initialization code is executed first, followed by the pretest condition. If the pretest condition evaluates to true, then the body of the loop is executed. After the body is executed, the post-execute code is run.The perceived encapsulation of the for loop makes it a favorite of developers.
var i = 0; while(i < 10){ //loop body i++; }Before the loop body is executed, the pretest condition is evaluated. If the condition evaluates to true, then the loop body is executed; otherwise, the loop body is skipped. Any for loop can also be written as a while loop and vice versa.
var i = 0; do { //loop body } while (i++ < 10);
for (var prop in object){ //loop body }Each time the loop is executed, the prop variable is filled with the name of another property (a string) that exists on the object until all properties have been returned. The returned properties are both those that exist on the object instance and those inherited through its prototype chain.
var props = ["prop1", "prop2"], i = 0; while (i < props.length){ process(object[props[i]]); }This code creates an array whose members are property names. The while loop is used to iterate over this small number of properties and process the appropriate member on object. Rather than looking up each and every property on object, the code focuses on only the properties of interest, saving loop overhead and time.(You should never use for-in to iterate over members of an array.)
//original loops for (var i=0; i < items.length; i++){ process(items[i]); } var j=0; while (j < items.length){ process(items[j++]]); } var k=0; do { process(items[k++]); } while (k < items.length);
//minimizing property lookups for (var i=0, len=items.length; i < len; i++){ process(items[i]); } var j=0, count = items.length; while (j < count){ process(items[j++]]); } var k=0, num = items.length; do { process(items[k++]); } while (k < num);Each of these rewritten loops makes a single property lookup for the array length prior to the loop executing. This allows the control condition to be comprised solely of local variables and therefore run much faster. Depending on the length of the array, you can save around 25% off the total loop execution time in most browsers (and up to 50% in Internet Explorer).
//minimizing property lookups and reversing for (var i=items.length; i--; ){ process(items[i]); } var j = items.length; while (j--){ process(items[j]]); } var k = items.length-1; do { process(items[k]); } while (k--);The loops in this example are reversed and combine the control condition with the decrement operation. Each control condition is now simply a comparison against zero. Control conditions are compared against the value true, and any nonzero number is automatically coerced to true, making zero the equivalent of false. Effectively, the control condition has been changed from two comparisons (is the iterator less than the total and is that equal to true?) to just a single comparison (is the value true?). Cutting down from two comparisons per iteration to one speeds up the loops even further. By reversing loops and minimizing property lookups, you can see execution times that are up to 50%–60% faster than the original. As a comparison to the originals, here are the operations being performed per iteration for these loops:
//credit: Jeff Greenberg var iterations = Math.floor(items.length / 8), startAt = items.length % 8, i = 0; do { switch(startAt){ case 0: process(items[i++]); case 7: process(items[i++]); case 6: process(items[i++]); case 5: process(items[i++]); case 4: process(items[i++]); case 3: process(items[i++]); case 2: process(items[i++]); case 1: process(items[i++]); } startAt = 0; } while (--iterations);The basic idea behind this Duff’s Device implementation is that each trip through the loop is allowed a maximum of eight calls to process(). The number of iterations through the loop is determined by dividing the total number of items by eight. Because not all numbers are evenly divisible by eight, the startAt variable holds the remainder and indicates how many calls to process() will occur in the first trip through the loop. If there were 12 items, then the first trip through the loop would call process() 4 times, and then the second trip would call process() 8 times, for a total of two trips through the loop instead of 12.
//credit: Jeff Greenberg var i = items.length % 8; while(i){ process(items[i--]); } i = Math.floor(items.length / 8); while(i){ process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); process(items[i--]); }Even though this implementation is now two loops instead of one, it runs faster than the original by removing the switch statement from the loop body.
items.forEach(function(value, index, array){ process(value); });The forEach() method is implemented natively in Firefox, Chrome, and Safari. Additionally, most JavaScript libraries have the logical equivalent:
//YUI 3 Y.Array.each(items, function(value, index, array){ process(value); }); //jQuery jQuery.each(items, function(index, value){ process(value); }); //Dojo dojo.forEach(items, function(value, index, array){ process(value); }); //Prototype items.each(function(value, index){ process(value); }); //MooTools $each(items, function(value, index){ process(value); });Even though function-based iteration represents a more convenient method of iteration,it is also quite a bit slower than loop-based iteration. The slowdown can be accounted for by the overhead associated with an extra method being called on each array item. In all cases, function-based iteration takes up to eight times as long as loop-based iteration and therefore isn’t a suitable approach when execution time is a significant concern.
if (found){ //do something } else { //do something else } switch(found){ case true: //do something break; default: //do something else }Though both pieces of code perform the same task, many would argue that the ifelse statement is much easier to read than the switch. Increasing the number of conditions, however, usually reverses that opinion:
if (color == "red"){ //do something } else if (color == "blue"){ //do something } else if (color == "brown"){ //do something } else if (color == "black"){ //do something } else { //do something } switch (color){ case "red": //do something break; case "blue": //do something break; case "brown": //do something break; case "black": //do something break; default: //do something }Most would consider the switch statement in this code to be more readable than the if-else statement.
if (value < 5) { //do something } else if (value > 5 && value < 10) { //do something } else { //do something }This code is optimal only if value is most frequently less than 5. If value is typically greater than or equal to 10, then two conditions must be evaluated each time before the correct path is taken, ultimately increasing the average amount of time spent in this statement. Conditions in an if-else should always be ordered from most likely to least likely to ensure the fastest possible execution time.
if (value == 0){ return result0; } else if (value == 1){ return result1; } else if (value == 2){ return result2; } else if (value == 3){ return result3; } else if (value == 4){ return result4; } else if (value == 5){ return result5; } else if (value == 6){ return result6; } else if (value == 7){ return result7; } else if (value == 8){ return result8; } else if (value == 9){ return result9; } else { return result10; }With this if-else statement, the maximum number of conditions to evaluate is 10. This slows down the average execution time if you assume that the possible values for value are evenly distributed between 0 and 10. To minimize the number of conditions to evaluate, the code can be rewritten into a series of nested if-else statements, such as:
if (value < 6){ if (value < 3){ if (value == 0){ return result0; } else if (value == 1){ return result1; } else { return result2; } } else { if (value == 3){ return result3; } else if (value == 4){ return result4; } else { return result5; } } } else { if (value < 8){ if (value == 6){ return result6; } else { return result7; } } else { if (value == 8){ return result8; } else if (value == 9){ return result9; } else { return result10; } } }The rewritten if-else statement has a maximum number of four condition evaluations each time through. This is achieved by applying a binary-search-like approach, splitting the possible values into a series of ranges to check and then drilling down further in that section. The average amount of time it takes to execute this code is roughly half of the time it takes to execute the previous if-else statement when the values are evenly distributed between 0 and 10. This approach is best when there are ranges of values for which to test (as opposed to discrete values, in which case a switch statement is typically more appropriate).
switch(value){ case 0: return result0; case 1: return result1; case 2: return result2; case 3: return result3; case 4: return result4; case 5: return result5; case 6: return result6; case 7: return result7; case 8: return result8; case 9: return result9; default: return result10; }The amount of space that this switch statement occupies in code is probably not proportional to its importance. The entire structure can be replaced by using an array as a lookup table:
//define the array of results var results = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10]; //return the correct result return results[value];When using a lookup table, you have completely eliminated all condition evaluations. The operation becomes either an array item lookup or an object member lookup. This is a major advantage for lookup tables: since there are no conditions to evaluate, there is little or no additional overhead as the number of possible values increases. Lookup tables are most useful when there is logical mapping between a single key and a single value (as in the previous example). A switch statement is more appropriate when each key requires a unique action or set of actions to take place.
function factorial(n){ if (n == 0){ return 1; } else { return n * factorial(n-1); } }The problem with recursive functions is that an ill-defined or missing terminal condition can lead to long execution times that freeze the user interface. Further, recursive functions are more likely to run into browser call stack size limits.
try { recurse(); } catch (ex){ alert("Too much recursion!"); }If left unhandled, these errors bubble up as any other error would (in Firefox, it ends up in the Firebug and error consoles; in Safari/Chrome it shows up in the JavaScript console), except in Internet Explorer. IE will not only display a JavaScript error, but will also display a dialog box that looks just like an alert with the stack overflow message.
function recurse(){ recurse(); } recurse();This pattern is typically easy to identify when errors occur. A second, subtler pattern involves two functions:
function first(){ second(); } function second(){ first(); } first();In this recursion pattern, two functions each call the other, such that an infinite loop is formed. This is the more troubling pattern and a far more difficult one to identify in large code bases.
function merge(left, right){ var result = []; while (left.length > 0 && right.length > 0){ if (left[0] < right[0]){ result.push(left.shift()); } else { result.push(right.shift()); } } return result.concat(left).concat(right); } function mergeSort(items){ if (items.length == 1) { return items; } var middle = Math.floor(items.length / 2), left = items.slice(0, middle), right = items.slice(middle); return merge(mergeSort(left), mergeSort(right)); }The code for this merge sort is fairly simple and straightforward, but the mergeSort() function itself ends up getting called very frequently. An array of n items ends up calling mergeSort() 2 * n –1 times, meaning that an array with more than 1,500 items would cause a stack overflow error in Firefox.
//uses the same mergeSort() function from previous example function mergeSort(items){ if (items.length == 1) { return items; } var work = []; for (var i=0, len=items.length; i < len; i++){ work.push([items[i]]); } work.push([]); //in case of odd number of items for (var lim=len; lim > 1; lim = (lim+1)/2){ for (var j=0,k=0; k < lim; j++, k+=2){ work[j] = merge(work[k], work[k+1]); } work[j] = []; //in case of odd number of items } return work[0]; }This implementation of mergeSort() does the same work as the previous one without using recursion. Although the iterative version of merge sort may be somewhat slower than the recursive option, it doesn’t have the same call stack impact as the recursive version. Switching recursive algorithms to iterative ones is just one of the options for avoiding stack overflow errors.
var fact6 = factorial(6); var fact5 = factorial(5); var fact4 = factorial(4);This code produces three factorials and results in the factorial() function being called a total of 18 times. The worst part of this code is that all of the necessary work is completed on the first line. Since the factorial of 6 is equal to 6 multiplied by the factorial 5, the factorial of 5 is being calculated twice. Even worse, the factorial of 4 is being calculated three times. It makes far more sense to save those calculations and reuse them instead of starting over anew with each function call. You can rewrite the factorial() function to make use of memoization in the following way:
function memfactorial(n){ if (!memfactorial.cache){ memfactorial.cache = { "0": 1, "1": 1 }; } if (!memfactorial.cache.hasOwnProperty(n)){ memfactorial.cache[n] = n * memfactorial(n-1); } return memfactorial.cache[n]; }The key to this memoized version of the factorial function is the creation of a cache object. This object is stored on the function itself and is prepopulated with the two simplest factorials: 0 and 1. Before calculating a factorial, this cache is checked to see whether the calculation has already been performed. No cache value means the calculation must be done for the first time and the result stored in the cache for later usage. This function is used in the same manner as the original factorial() function:
var fact6 = memfactorial(6); var fact5 = memfactorial(5); var fact4 = memfactorial(4);This code returns three different factorials but makes a total of eight calls to memfactorial(). Since all of the necessary calculations are completed on the first line, the next two lines need not perform any recursion because cached values are returned. The memoization process may be slightly different for each recursive function, but generally the same pattern applies. To make memoizing a function easier, you can define a memoize() function that encapsulates the basic functionality. For example:
function memoize(fundamental, cache){ cache = cache || {}; var shell = function(arg){ if (!cache.hasOwnProperty(arg)){ cache[arg] = fundamental(arg); } return cache[arg]; }; return shell; }This memoize() function accepts two arguments: a function to memoize and an optional cache object. The cache object can be passed in if you’d like to prefill some values; otherwise a new cache object is created. A shell function is then created that wraps the original (fundamental) and ensures that a new result is calculated only if it has never previously been calculated. This shell function is returned so that you can call it directly, such as:
//memoize the factorial function var memfactorial = memoize(factorial, { "0": 1, "1": 1 }); //call the new function var fact6 = memfactorial(6); var fact5 = memfactorial(5); var fact4 = memfactorial(4);Generic memoization of this type is less optimal that manually updating the algorithm for a given function because the memoize() function caches the result of a function call with specific arguments. Recursive calls, therefore, are saved only when the shell function is called multiple times with the same arguments. For this reason, it’s better to manually implement memoization in those functions that have significant performance issues rather than apply a generic memoization solution.