Appearance or Efficiency

There are a number of web sites that show you how to change the way that you code your JavaScript to make it more efficient. In many cases the efficiency of the code is the only thing that the author of those sites is concerned with and some of the code they recommend can be much harder to read than the original while providing an efficiency gain so small that you'd have to run the code millions of times in order for the gain to be big enough for anyone to notice.

One such example that I saw started off with the following code to return different values depending on the value in another field - where that value can be anything between 0 and 10. Here's the code that they assumed that you'd be starting with:

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;
}

Now to start with you should never use == in JavaScript, those comparisons should be using === but that has no effect on this particular discussion as the best solutions don't use that operator at all.

The code that they suggested replacing it with reorganises the if statements so that instead of needing to process anywhere between one and ten if comparisons to get to the appropriate return statement that each execution would run either three or four comparisons.

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;
      }
   }
}

Now there are a few things wrong with their solution including the fact that their code is not really any more efficient than the first version if you assume that the code is being called enough times for any efficiency improvements to be even measurable.

The original code makes no assumptions about the allowable values needing to be continuous. With that original version the first ten values can be any ten values and the eleventh result acts as a catchall for if the value is not one of those ten. Their code specifically assumes that the eleventh value is greater than the first ten and assumes that we know the order that the values are in so as to be able to pick the middle or closest to middle value each time to be able to divide the possible results in two each time. Suppose that the eleven values were (@Gv9h#M2) which are certainly not in ascending order the way I have listed them. Now substituting those eleven values in the original code in place of 0 through 10 is trivially easy. Substituting them into the proposed replacement code means that you need to order them correctly first. Also since the order is not obvious the proposed replacement code which is not as readable as the first version will be a lot less readable assuming that you manage to substitute the values correctly.

Also the author of the page that proposed that replacement code is under the mistaken impression that the second version is more efficient than the first. That is true but only by a very small amount and only if the eleven possible values are equally likely to occur. That second version gives on average 3.36364 comparisons each time the code is called. The original far easier to read version gives an average of 5 comparisons each time the code is called when the values are equally likely. This means that if you called this code 1000 times then you'd save approximately 1600 comparisons if you used the second version. You'd need to call the code quite a few million times before the saving resulted in a noticeable time saving in the running of that code. The efficiency improvement is small and the decrease in the maintainability of the code is much larger.

Another consideration is that the second version of the code is assuming that the values are equally likely or close to equally likely. This is not necessarily going to be the case. If instead some values are more likely than others and we can identify the more likely ones then that second harder to read version becomes the least efficient way of coding the comparisons. Suppose that we knew that smaller numbers were more likely to occur than larger ones and that each number was 1.2 times more likely to occur than the the next number - that is that 0 is 1.2 times more likely than 1 and 1 is 1.2 times more likely than 2 etc. This reduces the average number of comparisons in the first version of the code under 3 making that version more efficient than the second version. By placing the more likely values earlier in the list using the first version of the code we can retain the more readable version and get potential efficiency gains greater than that with the second version.

Anyway, neither of those alternatives is the way that you should be coding that particular comparison. What we can do is to pass off exactly how to perform these comparisons most efficiently to the browser and make our code even more readable at the same time. What we should be doing to improve on that original version in order to do this is to substitute a single switch statement for the ten comparisons.

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;
}

Now we have even more readable code and at worst the comparisons will be done the same way as in the first version. This means that it might be fractionally less efficient than that second messy hard to read version but will be at least as efficient than the first version - the comparisons may run faster given that the browser knows at the start that it is comparing a single field with multiple values rather than each comparison being separate so it may potentially be faster than the second version even with evenly distributed values.

Now the switch version makes no assumptions on what the eleven values can be just as with the first version. The second version made the assumption about the order of the eleven values. With the particular code shown the values used are 0 through 10 although as we already considered, that code can work with any eleven values provided you know what order to put them in. Now if we know that the first ten of the eleven values are the numbers 0 through 9 then an even more efficient and much easier to read way of rewriting this code is to put those first ten possible results into an array. The appropriate value can then be retrieved from the array with the only comparison required being to test that it actually is one of those ten values. That is if we assume that the last value can be anything else other than those values as the first and third versions of the code allow.

var retArray = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9];
if (value >= 0 && value <= 9) return resArray[value];
else return result10;

If instead we assume that the eleventh possible value will always be 10 (just as the second version assumes that the eleventh value will always be larger than the first ten values then we can simplify the entire thing into a single statement with no comparisons at all.

return [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10][value];

So the situations where that second version of the code is actually almost readable because of them being integers starting from zero rather than all sorts of different string values there is no point in using it at all as this far more efficient alternative to do away with all the comparisons exists. Where the values are not a continuous range of integers the readability and maintainability of the switch statement by far outweighs any efficiency gains that second alternative would give in the vast majority of situations. Remembering that the second version using if statements would need to be completely rewritten if another value were to be added to the possible results and except in the rarest of cases (such as if the code is running billions of times with each result equally likely) the efficiency gains will be to small to notice. Offsetting the maintenance cost against the efficiency gain means that version two will be the most costly variant of this code in all but the rarest of situations.

Note: The point that the person who posted those first two code patterns was trying to make is perfectly valid - a binary search is usually more efficient than a sequential search. It is just the particular example code they chose to demonstrate it with was poorly chosen. There could well be situations in JavaScript where converting a sequential search to a binary one is appropriate but the code will not look anything like those examples.

 

This article written by Stephen Chapman, Felgall Pty Ltd.

go to top

FaceBook Follow
Twitter Follow
Donate