衡量JavaScript函数的性能

性能一直在软件中起着至关重要的作用。 在网络上,性能更加重要,因为如果我们向用户提供慢速页面,他们可以轻松地更改网站并访问我们的竞争对手之一。 作为专业的Web开发人员,我们必须考虑到此问题。 今天,许多旧的Web性能优化最佳实践(例如,使请求最小化,使用CDN和不编写渲染阻止代码)仍然适用。 但是,随着越来越多的Web应用程序使用JavaScript,重要的是要验证我们的代码是否快速。

假设您具有工作功能,但是您怀疑它没有达到预期的速度,并且您有改善它的计划。 您如何证明这一假设? 今天测试JavaScript函数性能的最佳实践是什么? 通常,完成此任务的最佳方法是使用内置的performance.now()函数并测量函数执行前后的时间。

在本文中,我们将讨论如何衡量代码执行时间和技术,以避免一些常见的陷阱。

Performance.now()

高分辨率时间API提供了一个名为now()的函数,该函数返回DOMHighResTimeStamp对象。 这是一个浮点数,它以毫秒为单位反映当前时间, 精确到千分之一毫秒 。 单独地,该数字并不能为您的分析增加太多价值,但是两个这样的数字之间的差给出了经过多少时间的准确描述。

除了比内置的Date对象更准确之外,它也是“单调的”。 简而言之,这意味着它不受系统(例如您的笔记本电脑操作系统)定期校正系统时间的影响。 用更简单的术语来说,定义Date两个实例并计算差值并不代表已过去的时间。

“单调”的数学定义(以函数或数量表示)以这样的方式变化,即从不减少或从不增加

解释它的另一种方法是,尝试想象在时钟向前或向后的一年中的某个时间使用它。 例如,当您所在国家/地区的时钟都同意跳过一个小时以使日照最大化时。 如果您要在时钟返回一个小时之前创建一个Date实例,然后在时钟返回一个小时之前创建另一个Date实例,则查看差异时会说“ 1小时3秒123毫秒”。 对于performance.now()的两个实例,差异将是“ 3秒123毫秒和456789千毫秒”。

在本节中,我不会详细介绍此API。 因此,如果您想了解更多有关它的信息并查看其用法示例,建议您阅读文章发现高分辨率时间API

既然您知道了高分辨率时间API是什么以及如何使用它,那么让我们来探究一些潜在的陷阱。 但是在这样做之前,让我们定义一个名为makeHash()的函数,该函数将在本文的其余部分中使用。

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

可以如下测量该功能的执行情况:

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

如果在浏览器中运行此代码,则应看到类似以下内容的内容:

Took 0.2730 milliseconds to generate: 77005292

该代码的实时演示如下所示:

请参阅CodePen上的SitePoint( @SitePoint )提供的Pen YXmdNJ

考虑到这个例子,让我们开始讨论。

陷阱#1 –偶然测量不重要的事物

在上面的示例中,您可以注意到,我们在一个performance.now()和另一个performance.now()之间要做的唯一事情是调用函数makeHash()并将其值分配给变量result 。 这为我们提供了执行该功能所需的时间,仅此而已。 也可以按以下详细方法进行此测量:

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

该片段的实时演示如下所示:

请参阅CodePen上的SitePoint( @SitePoint )提供的Pen PqMXWv

但是,在这种情况下,我们会衡量它需要调用函数时makeHash('Peter') 以及需要多长时间来发送和打印控制台上的输出。 我们不知道这两个操作分别花费了多长时间。 您只知道合计时间。 同样,发送和打印输出所花费的时间也将大大不同,这取决于浏览器甚至当时的运行状况。

也许您完全意识到console.log缓慢。 但是,即使每个功能不涉及任何I / O,执行多个功能同样是错误的。 例如:

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

同样,我们将不知道执行时间是如何分配的。 是变量分配, toLowerCase()调用还是toString()调用?

陷阱2 –仅测量一次

另一个常见的错误是仅进行一次测量,总结所花费的时间并在此基础上得出结论。 在不同的时间可能完全不同。 执行时间很大程度上取决于各种因素:

  • 编译器预热的时间(例如,将代码编译为字节码的时间)
  • 主线程正在忙着做我们没有意识到的其他事情
  • 您计算机的CPU忙于某些事情,这会降低整个浏览器的速度

增量改进是重复执行该函数,如下所示:

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');

该示例的实时演示如下所示:

请参阅CodePen上的SitePoint( @SitePoint )提供的Pen Qbezpj

这种方法的风险在于,我们的浏览器的JavaScript引擎可能会进行子优化,这意味着第二次使用相同的输入调用该函数时,可以记住第一个输出并再次使用该输出,从而从中受益。 要解决此问题,您可以使用许多不同的输入字符串,而不必重复发送相同的输入字符串(例如'Peter' )。 显然,使用不同输入进行测试的问题在于,我们正在测量的功能自然需要花费不同的时间。 也许某些输入比其他输入导致更长的执行时间。

陷阱3 –过于依赖平均水平

在上一节中,我们了解到重复运行某些东西(理想情况下使用不同的输入)是一种很好的做法。 但是,我们必须记住,不同输入的问题在于执行时间可能比所有其他输入要长得多。 因此,让我们退后一步,发送相同的输入。 假设我们发送相同的输入十次,并且每次输入打印多长时间。 输出可能如下所示:

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

请注意,这个数字第一次与其他九次完全不同。 这很可能是因为我们浏览器中的JavaScript引擎进行了一些次优化,并且需要进行一些预热。 我们几乎无法避免这种情况,但是我们可以考虑采取一些好的补救措施来防止得出错误的结论。

一种方法是计算最近九次的平均值。 另一种更实用的方法是收集所有结果并计算中位数 。 基本上,所有结果都排好队,按顺序排序,然后选择中间的结果。 这是performance.now()如此有用的地方,因为您可以获得一个数字,您可以用它做任何事情。

让我们再试一次,但是这次我们将使用中值函数:

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');

陷阱4 –以可预测的顺序比较功能

我们知道,多次测量并取平均值始终是一个好主意。 而且,最后一个示例告诉我们,最好使用中位数而不是平均值。

现在,实际上,衡量函数执行时间的一个好方法是学习几个函数中哪个更快。 假设我们有两个函数,它们使用相同的输入类型并产生相同的结果,但在内部它们的工作方式不同。

假设我们希望有一个函数,如果某个字符串位于其他字符串数组中,则返回truefalse ,但是这种情况不区分大小写 。 换句话说,我们不能使用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

立即我们注意到,由于haystack.forEach循环始终遍历所有元素,因此即使我们有早匹配项,它也可以得到改进。 让我们尝试使用旧的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

现在,让我们看看哪一个是最快的。 为此,我们将每个功能运行10次并收集所有测量值:

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

我们运行它并获得以下输出:

true
false
true
false
isIn1 took 0.0050
isIn2 took 0.0150

该示例的实时演示如下所示:

请参阅CodePen上的SitePoint( @SitePoint )提供的Pen YXmdZJ

到底发生了什么事? 第一项功能快了三倍 。 那不应该发生的!

解释很简单但很微妙。 使用haystack.forEach的第一个函数受益于浏览器JavaScript引擎中的一些低级优化,而使用数组索引技术时我们无法获得这些优化。 这证明了我们的观点:要衡量就永远不知道!

结论

在尝试演示如何使用performance.now()获得准确的JavaScript执行时间时,我们偶然发现了一个基准测试场景,在该场景中,我们的直觉与实证结果的结论完全相反。 关键是,如果要编写更快的Web应用程序,则需要优化JavaScript代码。 由于计算机(几乎)是可以呼吸的东西,因此它们是不可预测的且令人惊讶。 知道我们的代码改进可以更快地执行的最可靠方法是测量和比较。

如果我们有多种方式来做同一件事,我们永远都不知道哪个代码更快,另一个原因是因为上下文很重要。 在上一节中,我们执行不区分大小写的字符串搜索,以在其他26个字符串中查找一个字符串。 如果我们不得不在100,000个其他字符串中寻找一个字符串,则结论可能会完全不同。

上面的列表并不详尽,因为还有更多陷阱需要注意。 例如,测量不现实的场景或仅测量一个JavaScript引擎。 但是可以肯定的是,对于想要编写更快,更好的Web应用程序的JavaScript开发人员来说,一项重要资产就是performance.now() 。 最后但并非最不重要的一点是,请记住,衡量执行时间只会产生“更好的代码”的一个维度。 还需要记住内存和代码复杂性方面的考虑。

你呢? 您是否曾经使用过此功能来测试代码的性能? 如果没有,您在此阶段如何进行? 请在下面的评论中分享您的想法。 让我们开始讨论!

From: https://www.sitepoint.com/measuring-javascript-functions-performance/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值