Developers Club geek daily blog

1 year, 5 months ago
Performance measurement of functions in JavaScript

Performance always played a key role in the software. And in web applications its value is even higher as users can easily go to competitors if the website made by you works slowly. Any professional web developer has to remember it. Today it is still possible to apply successfully the mass of old acceptances of optimization of performance, like minimization of amount of requests, use of CDN and not use to rendering of the blocking code. But the more developers apply JavaScript, the problem of optimization of its code becomes more important.

Possibly, you have certain suspicions concerning performance of the functions which are often used by you. Perhaps, you even estimated as it is possible to improve a situation. But how you will measure performance gain? How it is possible to test precisely and quickly performance of functions in JavaScript? Ideal option — to use the embedded function performance.now() and to measure time before execution of your functions. Here we will consider how it becomes, and also we will sort a number of reefs.

Performance.now ()


In High Resolution Time API there is a function now(), DOMHighResTimeStamp returning object. This floating-point number reflecting the current time in milliseconds with an accuracy of thousand millisecond. This number in itself has for us not enough value, but a difference between two measured values describes how many passed time.

Besides that this tool more precisely, than built-in object Date, it also "monotonous". If simply: it is not influenced by correction of system time. That is, having created two copies Date and having calculated between them a difference, we will not gain faithful, representative representation about that how many passed time.

From the point of view of mathematics monotonic function either only increases, or only decreases. Other example, for the best understanding: transition to daylight or winter saving time when the whole clock in the country is put back an hour or the hour ahead. If we compare values of two copies Date — before time conversion, we will receive, for example, a difference "1 hour 3 seconds and 123 milliseconds". And when using two copies performance.now() — "3 seconds 123 milliseconds of 456 789 thousand milliseconds". Here we will not sort in detail this API, persons interested can address the article Discovering the High Resolution Time API.

So, now we know that such High Resolution Time API and as to use it. Let's consider some possible errors now, but at first let's write function makeHash(), which will be used further in the text.

function makeHash(source) {
  var hash = 0;
  if (source.length === 0) return hash;
  for (var i = 0; i < source.length; i++) {
    var char = source.charCodeAt(i);
    hash = ((hash<<5)-hash)+char;
    hash = hash &hash; // Convert to 32bit integer
  }
  return hash;
}

Execution of similar functions can be measured by the following method:

var t0 = performance.now();
var result = makeHash('Peter');
var t1 = performance.now();
console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);

If to execute this code in the browser, then the result will look so:

Took 0.2730 milliseconds to generate: 77005292

Demo: codepen.io/SitePoint/pen/YXmdNJ

Error No. 1: accidental measurement of unnecessary things


In the example given above you could notice that between two performance.now() function is used makeHash(), whose value is appropriated to a variable result. So we calculate, what is the time occupied execution of this function, and nothing any more. It is possible to measure and in such a way:

var t0 = performance.now();
console.log(makeHash('Peter'));  // Bad idea!
var t1 = performance.now();
console.log('Took', (t1 - t0).toFixed(4), 'milliseconds');

Demo: codepen.io/SitePoint/pen/PqMXWv

But in this case we would measure, what is the time occupied function call makeHash('Peter'), and also duration of sending and an output of result in the console. We do not know, each of these operations what is the time borrows, only their general duration is known to us. Besides, the speed of sending data and an output in the console strongly depends on the browser and even on what else it does at this time. Possibly, you consider that it console.log works it is unpredictable slowly. But will execute an error anyway more than one function even if each of functions does not mean any input-output operations. For example:

var t0 = performance.now();
var name = 'Peter';
var result = makeHash(name.toLowerCase()).toString();
var t1 = performance.now();
console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to generate:', result);

Besides we do not know what operation took most of all time: variable value assignment, challenge toLowerCase() or toString().

Error No. 2: single measurement


Many take only one measurement, put the general time and draw far-reaching conclusions. But the situation every time can change, execution speed strongly depends on such factors as:

  • compilation time of a code in byte code (time of "warming up" of the compiler),
  • employment of the main process by execution of other tasks,
  • load of a CCP something because of what stops all browser.

Therefore it is better to execute not one measurement, and a little:

var t0 = performance.now();
for (var i = 0; i < 10; i++) {
  makeHash('Peter');
}
var t1 = performance.now();
console.log('Took', ((t1 - t0) / 10).toFixed(4), 'milliseconds to generate');

Demo: codepen.io/SitePoint/pen/Qbezpj

The risk of this approach is that the browser JavaScript-engine can execute suboptimization, i.e. for the second time function will be caused with the same input data which will be remembered and used further. That to bypass it, it is possible to use many different input strings instead of over and over again taking the same value. However at different input data and the speed of execution of function of times from time can differ.

Error No. 3: excessive trust to mean values


So, it is reasonable to do a series of measurements more precisely to evaluate performance of this or that function. But how to determine function performance if at different input data it is executed with a different speed? Let's experiment at first and we will measure runtime ten times with the same input data. Results will look approximately so:

Took 0.2730 milliseconds to generate: 77005292
Took 0.0234 milliseconds to generate: 77005292
Took 0.0200 milliseconds to generate: 77005292
Took 0.0281 milliseconds to generate: 77005292
Took 0.0162 milliseconds to generate: 77005292
Took 0.0245 milliseconds to generate: 77005292
Took 0.0677 milliseconds to generate: 77005292
Took 0.0289 milliseconds to generate: 77005292
Took 0.0240 milliseconds to generate: 77005292
Took 0.0311 milliseconds to generate: 77005292

Pay attention how very first value differs from the others. Most likely, the reason just in carrying out suboptimization and in need of "warming up" of the compiler. Little it is possible to make that to avoid it, but itself can secure against the incorrect conclusions.

For example, it is possible to exclude the first value and to calculate arithmetic-mean of other nine. But it is better to take all results and to calculate a median. Results are sorted by an order, and average is selected. Here where performance.now() it is very useful because you receive value with which it is possible to do anything.

So, let's measure again, but this time we use median value of selection:

var numbers = [];
for (var i=0; i < 10; i++) {
  var t0 = performance.now();
  makeHash('Peter');
  var t1 = performance.now();
  numbers.push(t1 - t0);
}

function median(sequence) {
  sequence.sort();  // note that direction doesn't matter
  return sequence[Math.ceil(sequence.length / 2)];
}

console.log('Median time', median(numbers).toFixed(4), 'milliseconds');

Error No. 4: comparison of functions in a predictable order


Now we know that it is always better to do several measurements and to take an average. Moreover, the last example says that ideally it is necessary to take a median instead of an average.

It is good to use measurement of runtime for the choice of the fastest function. Let's say we have two functions using identical input data and issuing identical results, but working differently. Let's tell, we need to select function which returns true or false if finds in an array a certain line, at the same time irrespective of the register. In this case we cannot use Array.prototype.indexOf.

function isIn(haystack, needle) {
  var found = false;
  haystack.forEach(function(element) {
    if (element.toLowerCase() === needle.toLowerCase()) {
      found = true;
    }
  });
  return found;
}

console.log(isIn(['a','b','c'], 'B'));  // true
console.log(isIn(['a','b','c'], 'd'));  // false

This code can be improved as a cycle haystack.forEach will touch all elements even if we quickly found coincidence. Let's use old kind for:

function isIn(haystack, needle) {
  for (var i = 0, len = haystack.length; i < len; i++) {
    if (haystack[i].toLowerCase() === needle.toLowerCase()) {
      return true;
    }
  }
  return false;
}

console.log(isIn(['a','b','c'], 'B'));  // true
console.log(isIn(['a','b','c'], 'd'));  // false

Now we will look what option quicker. Let's execute each function on ten times and we will calculate the "correct" results:

function isIn1(haystack, needle) {
  var found = false;
  haystack.forEach(function(element) {
    if (element.toLowerCase() === needle.toLowerCase()) {
      found = true;
    }
  });
  return found;
}

function isIn2(haystack, needle) {
  for (var i = 0, len = haystack.length; i < len; i++) {
    if (haystack[i].toLowerCase() === needle.toLowerCase()) {
      return true;
    }
  }
  return false;
}

console.log(isIn1(['a','b','c'], 'B'));  // true
console.log(isIn1(['a','b','c'], 'd'));  // false
console.log(isIn2(['a','b','c'], 'B'));  // true
console.log(isIn2(['a','b','c'], 'd'));  // false

function median(sequence) {
  sequence.sort();  // note that direction doesn’t matter
  return sequence[Math.ceil(sequence.length / 2)];
}

function measureFunction(func) {
  var letters = 'a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z'.split(',');
  var numbers = [];
  for (var i = 0; i < letters.length; i++) {
    var t0 = performance.now();
    func(letters, letters[i]);
    var t1 = performance.now();
    numbers.push(t1 - t0);
  }
  console.log(func.name, 'took', median(numbers).toFixed(4));
}

measureFunction(isIn1);
measureFunction(isIn2);

Let's receive such result:

true
false
true
false
isIn1 took 0.0050
isIn2 took 0.0150

Demo: codepen.io/SitePoint/pen/YXmdZJ

How to understand it? The first function was three times faster. It cannot just be! The explanation is simple, but not obvious. The first function using haystack.forEach, wins due to low-level optimization at the level of the browser JS engine which does not become when using an index of an array. So you will not measure yet, you do not learn!

Outputs


Trying to show performance measurement accuracy in JavaScript with the help performance.now(), we found out that our intuition can bring us: empirical data did not match our assumptions at all. If you want to write fast web applications, then the JS code needs to be optimized. And as computers almost living beings, they are still capable to be unpredictable and to surprise us. So the best method to make the code quicker — to measure and compare.

One more reason why we cannot foreknow what option will be quicker, is that everything depends on a situation. In the last example we looked for coincidence among 26 values regardless of the register. But if we look for among 100 000 values, then the choice of function can be other.

The considered errors — not only possible. It is possible to add to them, for example, measurement of unrealistic scenarios or measurement only on one JS engine. But it is important to remember the main thing: if you want to create fast web applications, then it is better than the tool performance.now() to you not to find. However measurement of runtime — only one aspect. Performance is influenced also by use of memory and complexity of a code.

This article is a translation of the original post at habrahabr.ru/post/272087/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus