返回介绍

第一部分 类型和语法

第二部分 异步和性能

6.1 性能测试

发布于 2023-05-24 16:38:21 字数 4148 浏览 0 评论 0 收藏 0

好,现在是时候消除一些误解了。我敢打赌,如果被问到如何测试某个运算的速度(执行时间),绝大多数 JavaScript 开发者都会从类似下面的代码开始:

var start = (new Date()).getTime(); // 或者Date.now()

// 进行一些操作

var end = (new Date()).getTime();

 console.log( "Duration:", (end - start) );

如果这大致上就是你首先想到的,请举手。嗯,我想就是如此。这种方案有很多错误,不过别难过,我们会找到正确方法的。

这个测量方式到底能告诉你什么呢?理解它做了什么以及关于这个运算的执行时间不能提供哪些信息,就是学习如何正确测试 JavaScript 性能的关键所在。

如果报告的时间是 0 ,可能你会认为它的执行时间小于 1ms。但是,这并不十分精确。有些平台的精度并没有达到 1ms,而是以更大的递增间隔更新定时器。比如,Windows(也就是 IE)的早期版本上的精度只有 15ms,这就意味着这个运算的运行时间至少需要这么长才不会被报告为 0 !

还有,不管报告的时长是多少,你能知道的唯一一点就是,这个运算的这次特定的运行消耗了大概这么长时间。而它是不是总是以这样的速度运行,你基本上一无所知。你不知道引擎或系统在这个时候有没有受到什么影响,以及其他时候这个运算会不会运行得更快。

如果时长报告是 4 呢?你能更加确定它的运行需要大概 4ms 吗?不能。它消耗的时间可能要短一些,而且在获得 start 或 end 时间戳之间也可能有其他一些延误。

更麻烦的是,你也不知道这个运算测试的环境是否过度优化了。有可能 JavaScript 引擎找到了什么方法来优化你这个独立的测试用例,但在更真实的程序中是无法进行这样的优化的,那么这个运算就会比测试时跑得慢。

那么,能知道的是什么呢?很遗憾,根据前面提出的内容,我们几乎一无所知。这样低置信度的测试几乎无力支持你的任何决策。这个性能测试基本上是无用的。更坏的是,它是危险的,因为它可能提供了错误的置信度,不仅是对你,还有那些没有深入思考带来测试结果的条件的人员。

6.1.1 重复

“好吧,”你现在会说,“那就用一个循环把它包起来,这样整个测试的运行时间就会更长一些了。”如果重复一个运算 100 次,然后整个循环报告共消耗了 137ms,那你就可以把它除以 100,得到每次运算的平均用时为 1.37ms,是这样吗?

并不完全是这样。

简单的数学平均值绝对不足以对你要外推到整个应用范围的性能作出判断。迭代 100 次,即使只有几个(过高或过低的)的异常值也可以影响整个平均值,然后在重复应用这个结论的时候,你还会扩散这个误差,产生更大的欺骗性。

你也可以不以固定次数执行运算,转而循环运行测试,直到达到某个固定的时间。这可能会更可靠一些,但如何确定要执行多长时间呢?你可能会猜测,执行时间应该是你的运算执行的单次时长的若干倍。错。

实际上,重复执行的时间长度应该根据使用的定时器的精度而定,专门用来最小化不精确性。定时器的精度越低,你需要运行的时间就越长,这样才能确保错误率最小化。15ms 的定时器对于精确的性能测试来说是非常差劲的。要最小化它的不确定性(也就是出错率)到小于 1%,需要把你的每轮测试迭代运行 750ms。而 1ms 定时器时只需要每轮运行 50ms 就可以达到同样的置信度。

但是,这只是单独的一个例子。要确保把异常因素排除,你需要大量的样本来平均化。你还会想要知道最差样本有多慢,最好的样本有多快,以及最好和最差情况之间的偏离度有多大,等等。你需要知道的不仅仅是一个告诉你某个东西跑得有多快的数字,还需要得到某个可以计量的测量值告诉你这个数字的可信度有多高。

还有,你可能会想要把不同的技术(以及其他方面)组合起来,以得到所有可能方法的最佳平衡。

这仅仅是个开始。如果你过去进行性能测试的方法比我刚才提出的还要不正式的话,好吧,那么可以说你完全不知道:正确的性能测试。

6.1.2 Benchmark.js

任何有意义且可靠的性能测试都应该基于统计学上合理的实践。此处并不打算撰写一章关于统计学的内容,所以我要和如下术语挥手作别:标准差、方差、误差幅度。如果你不知道这些术语的意思——我回大学上了一门统计学课程,但对这些还是有点糊涂——那么实际上你还不够资格编写自己的性能测试逻辑。

幸运的是,像 John-David Dalton 和 Mathias Bynens 这样的聪明人了解这些概念,并编写了一个统计学上有效的性能测试工具,名为 Benchmark.js(http://benchmarkjs.com/ )。因此,对于这个悬而未决的问题,我的答案就是:“使用这个工具就好了。”

我并不打算复述他们的整个文档来介绍 Benchmark.js 如何运作。他们的 API 很不错,你应该读一读。还有一些很棒的文章介绍了更多的细节和方法,比如这里(http://calendar.perfplanet.com/2010/bulletproof-javascript-benchmarks )和这里(http://monsur.hosa.in/2012/12/11/benchmarksjs.html )。

但为了简单展示一下,下面介绍应该如何使用 Benchmark.js 来运行一个快速的性能测试:

function foo() {
  // 要测试的运算
}

var bench = new Benchmark(
  "foo test",       // 测试名称
  foo,          // 要测试的函数(也即内容)
  {
    // ..         // 可选的额外选项(参见文档)
  }
);

bench.hz;        // 每秒运算数
bench.stats.moe;     // 出错边界
bench.stats.variance;  // 样本方差
// ..

除了这里我们介绍的一点内容,关于 Benchmark.js 的使用还有很多要学的。但是,关键在于它处理了为给定的一段 JavaScript 代码建立公平、可靠、有效的性能测试的所有复杂性。如果你想要对你的代码进行功能测试和性能测试,这个库应该最优先考虑。

这里我们展示了测试一个像 X 这样的单个运算的使用方法,不过很可能你还想要比较 X 和 Y。通过在一个 suite(Benchmark.js 组织特性)中建立两个不同的测试很容易做到这一点。然后,可以依次运行它们,比较统计结果,得出结论,判断 X 和 Y 哪个更快。

Benchmark.js 当然可以用在浏览器中测试 JavaScript(参见 6.3 节),它也可以在非浏览器环境中运行(Node.js 等)。

Benchmark.js 有一个很大程度上还未开发的潜在用例,就是你可以将其用于开发或测试环境中,针对应用中 JavaScript 的关键路径部分运行自动性能回归测试。这和你可能在部署之前运行的单元测试套件类似,你也可以与之前的版本进行性能测试比较,以监控应用性能是提高了还是降低了。

setup/teardown

在前面的代码片段中,我们忽略了“额外选项”{ .. } 对象。这里有两个选项是我们应该讨论的:setup 和 teardown 。

这两个选项使你可以定义在每个测试之前和之后调用的函数。

有一点非常重要,一定要理解,setup 和 teardown 代码不会在每个测试迭代都运行。最好的理解方法是,想像有一个外层循环(一轮一轮循环)还有一个内层循环(一个测试一个测试循环)。setup 和 teardown 在每次外层循环(轮)的开始和结束处运行,而不是在内层循环中。

为什么这一点很重要呢?设想你有一个像这样的测试用例:

a = a + "w";
b = a.charAt( 1 );

然后,你建立了测试 setup 如下:

var a = "x";

你的目的可能是确保每个测试迭代开始的 a 值都是 "x" 。但并不是这样!只有在每一轮测试开始时 a 值为 "x" ,然后重复 + "w" 链接运算会使得 a 值越来越长,即使你只是访问了位置 1 处的字符 "w" 。

对某个东西,比如 DOM,执行产生副作用的操作的时候,比如附加一个子元素,常常会刺伤你。你可能认为你的父元素每次都清空了,但是,实际上它被附加了很多元素,这可能会严重影响测试结果。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文