【译】Javascript 基准测试

2014-02-18 W.Y. 更多博文 » 博客 » GitHub »

Performance

原文链接 https://bubkoo.github.io/2014/02/18/bulletproof-javascript-benchmarks/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


原文发表在 Performance Calendar 上,作为其 2010 年系列文章的一部分。在上一篇翻译的文章中,作者推荐了这篇文章,译者阅读之后觉得有一定的价值,而且网络上没有这篇文章的中文翻译,权当学习就在此翻译成了中文,水平有限,如果有表达不恰当或者表达有误的地方,请直接在评论中指出。

原文链接:Bulletproof JavaScript benchmarks,下面开始翻译正文。

编写 JavaScript 基准测试并不是想象的那么容易,撇开潜在的跨浏览器兼容问题不说,还将面临很多陷阱(甚至诡雷)。

这也是我创建 jsPerf 的一部分原因,jsPerf 提供了一个非常简单的 Web 接口,您可以非常轻松地创建和分享不同代码片段的性能测试用例。您不需要考虑其他问题,只需要输入你想做基准测试的代码,jsPerf 就会为您创建一个运行在不同的浏览器和设备上测试用例。

其实,jsPerf 最开始使用的是一个基于 JSLitmus 的基准测试库 - Benchmark.js 。后来添加了很多新功能,最近,John-David Dalton 又从头开始完全重写了该代码,Benchmark.js 也变得越来越完善。

本文将阐明编写和运行 JavaScript 基准测试的各种陷阱。 <!--more-->

基准测试的方式

目前,有很多方法可以对 JavaScript 代码片段进行性能基准测试。最常见的模式如下:

方式 A

var totalTime,
    start = new Date,
    iterations = 6;
while (iterations--) {
  // 您的 JavaScript 代码片段
}
// totalTime → 代码片段的总共执行时间
// 代码片段循环了 6 次
totalTime = new Date - start;

将要测试的代码片段放在一个循环中,并使其执行预定义的次数(这里是 6 次),然后,用结束时间减去开始时间就可以得到整个代码的执行时间。方式 A 被 SlickSpeedTaskspeedSunSpiderKraken 这样一些流行的基准测试组件采纳。

方式 A 的问题

由于浏览器和设备变得越来越快,方式 A 的测试很大可能会得到 0ms,这使得测试结果不可用。

方式 B

另一种方法是计算在指定时间段内可以执行多少次操作,这种方式的优点是不需要您指定一个迭代次数。

var hz,
    period,
    startTime = new Date,
    runs = 0;
do {
  // 您的代码片段
  runs++;
  totalTime = new Date - startTime;
} while (totalTime < 1000);

// 将毫秒转换成秒
totalTime /= 1000;

// period → 执行每个操作需要的时间
period = totalTime / runs;

// hz → 每秒可以执行多少个操作
hz = 1 / period;

// 可以简写为
// hz = (runs * 1000) / totalTime;

这里的代码片段大约执行了 1 秒钟,方式 B 被使用在 DromaeoV8 基准测试组件中。

方式 B 的问题

在进行这种基准测试时,由于垃圾回收机制、引擎优化和其他后台进程的影响,测试结果会有所不同。由于这种差异,就需要运行数次基准测试代码来取得平均结果。然而,在 V8 中每个基准测试只会运行 1 次,Dromaeo 也仅仅只会运行 5 次。我们可以通过运行更多次来减少误差幅度,方法之一是通过减少每次基准测试运行的时间,例如从 1000ms 减少到 50ms,这样在相同的时间内就可以运行更多次数的基准测试。

方式 C

JSLitmus 是基于上面这两种方式来构建的,它使用方式 A 来将一个测试运行 n 次,同时使用方式 B 来动态增加 n 来保持测试运行,直到达到最小测试时间。

方式 C 的问题

JSLitmus 避免了方式 A 的问题,但仍有方式 B 的问题。为了提高结果的准确性,JSLitmus 通过获取三次空测试中最快的那个时间,然后将每次基准测试的时间减去这个最快时间,来校准测试结果。不幸的是,这种方法混淆了最终结果,因为“获取 3 个空测试中的最快时间”不是一个统计上有效的方法。虽然 JSLitmus 运行基准测试数次,并且从基准测试的平均结果中减去了校验平均值,还是增加了最终结果的误差幅度,也吞噬了增加准确性的希望。

方式 D

方式 A、B 和 C 的缺点可以通过编译函数和展开循环来避免。

function test() {
  x == y;
}

while (iterations--) {
  test();
}

// ...would compile to →
var hz,
    startTime = new Date;

x == y;
x == y;
x == y;
x == y;
x == y;
// ...

hz = (runs * 1000) / (new Date - startTime);

方式 D 的问题

然而,这也有它的短板。编译函数会大大增加内存使用量和减慢您的CPU,当您重复运行测试几百万次时,基本上就等于创建了一个非常大的字符串和编译了一个庞大的函数。

使用展开循环的另一个警告是,return 语句可以使测试提前退出。花了很大成本去编译一个有数百万行代码的函数,然而这个函数在执行到第 3 行就返回了,这非常没有意义。有必要进行早期退出检测,如果有早期退出就回到使用 while 循环的模式,并在需要时通过循环校准。

提取函数体

Benchmark.js 使用了稍微有些不同的方法,可以说它使用了前面四种方式中的最好的部分。因为内存问题,我们不展开循环,为了减少可能会使结果不准确的因素,并允许测试访问本地方法和变量,我们在每个测试中提取出函数体。例如,当测试这样的代码时:

var x = 1,
    y = "1";

function test() {
  x == y;
}

while (iterations--) {
  test();
}

// ...would compile to →

var x = 1,
    y = "1";
while (iterations--) {
  x == y;
}

然后,Benchmark.js 使用了和 JSLitmus 类似的方法:将提取的函数体放在一个 while 循环中运行(方式 A),重复运作直到达到最小的运行次数(方式 B),并将整个过程重复数次,来得到有统计意义的结果。

需要考虑的问题

不准确的毫秒计时器

在一些浏览器/操作系统中,由于各种各样因素,计时器可能是不准确的。

例如:

当 Windows XP 启动后,典型的默认时钟中断期是 10 毫秒,尽管在一些系统使用的是 15 毫秒。这意味着,每 10 毫秒,操作系统就接收来自系统定时器硬件中断通知。

一些老的浏览器(IE,Firefox 2)依靠内部操作系统的定时器,这意味着每次调用 new Date().getTime() 都是直接从操作系统中获取。很明显,如果内部定时器每 10 或 15毫秒才更新,是的测试结果的准确性大大降低。我们需要解决这个问题。

幸运的是,可以使用 JavaScript 来获得最小测量单位,然后,通过一个数学方法来使我们的测试结果的不确定度减少到 1% 。要做到这一点,我们需要将测量的最小单位除以 2 来得到的不确定度。假如我们正在 Windows XP 上使用 IE6,最小测量单位是 15 毫秒,那么不确定度就是 15ms / 2 = 7.5ms,然后将其除以 0.01(1%),这样就得到了我们所需的最小测试时间是:7.5ms / 0.01 = 750ms。

替代计时器

当使用 --enable-benchmarking 标志来启动 Chrome 时,Chrome 将暴露 chrome.Interval 方法,这个可以用作一个高精度微秒计时器。

回到我们的 Benchmark.js,John-David Dalton 偶然发现了 Java 中的纳秒计时器,并通过一个微小的 Java 应用提供给 JavaScript 使用。

使用高精度计时器可以将测试时间分类,它允许更大的样本大小,减小了结果的误差幅度。

Firebug 会禁用 Firefox 的 JIT

开启 Firebug 插件会禁用 Firefox 所有的高性能实时(JIT)本地代码编译功能,这意味着你会在解释器运行这些测试,也就是说,您的测试将运行得非常缓慢。你应该永远记住,在 Firefox 下进行基准测试时要禁用 Firebug 插件。

虽然这个影响似乎要小得多,这同样也适用于有 inspector 工具的其他浏览器,比如 WebKit 的 Web Inspector 或 Opera 的 Dragonfly。在进行基准测试时避免这些打开这些工具,因为它可能会影响结果。

浏览器 bug 和特性

基准测试中某些形式的循环机制容易受到浏览器一些怪癖的影响,比如最近 IE9 的 dead-code-removal 的演示,Mozilla 浏览器的 TraceMonkey engine bug,还有 Opera 的 caching of qSA results 也将导致基准测试结果的不准确。当创建基准测试时,记住这些非常重要。

统计学意义

大多数基准测试产生的结果没都有统计学意义,John Resig 在他的文章(JavaScript benchmark quality)中讨论过这个问题。总之,有必要考虑每个结果的误差幅度,并尽可能减少。使用更大的样本量,并沉着等待测试完成,有助于减少误差幅度。

跨浏览器测试

如果你想在不同的浏览器下运行基准测试并得到可靠的结果,一定要在真正的浏览器环境中进行测试。不要信任 IE 的兼容模式 - 这些都不同于实际的浏览器版本

同时,请注意这一事实,IE(IE8 及其以下)将脚本的最大指令数限制为 500 万,而不是像其他浏览器一样,限制一个脚本的执行时间。在现代的硬件环境下,一个密集型 CPU 可以在半秒内触发这个脚本,如果你有一个相当快的系统,在 IE 中你可能会遇到“脚本警告”对话框,在这种情况下,最好的解决方案是修改您的 Windows 注册表,增加指令的数量。幸运的是,微软提供了一个简单的方法来做这个,所有你需要做的就是运行一个简单的“修复”向导,更好的是,在 IE9 中删除了这个愚蠢的限制。

结论

不管您只是运行一些基准测试,还是编写自己的测试套件,甚至是编写您自己的基准测试库,都比您在本文中看到的要复杂得多。Benchmark.js 和 jsPerf 每周更新一次,伴随着 bug 的修复、新的特性和一些提高测试结果的准确性的小技巧。如果您只想在当前流行的浏览器下做一些基准测试,那就不要重复造轮子。。。