原文:Bulletproof JavaScript benchmarks

做JavaScript的基准测试并没有想的那么简单。即使不考虑浏览器差异所带来的影响,也有很多难点-或者说陷阱需要面对。

这是为何我创建了jsPerf的一个原因,一个你可以轻松创建并分享各种代码片段对比结果的简单工具。用起来非常省事,只需把想要测试的代码录入然后jsPerf会为你创建好可以跨平台跑起来的测试用例。

内部实现上,最开始jsPerf用的是一个基于JSLitmus的基准测试库,我将它称作Benchmark.js。后续又往里面添加了更多的特性,最近,John-David Dalton干脆将这个库彻底重写了一遍。所以现在Benchmark.js已经比之前好很多了。

本文将对JavaScript基准测试的编写和运行有一定的参考意义。

基准测试的类型

有很多方法可以测试一段JavaScript代码的性能。最常见的做法是类似下面这样的:

方案A

var totalTime,
start = new Date,
iterations = 6;
while (iterations--) {
// 被测试的代码
}
// totalTime → 运行该测试代码6次需要的时间(单位:毫秒)
totalTime = new Date - start;

这种方案将被测试的代码循环执行多次直到预设值(本例为6次)。最后用结束时的时间减去开始的时间,得到运行的总时间。

方案A被用于SlickSpeed, Taskspeed, SunSpider, 和 Kraken这些流行的基准测试库中。

缺撼

鉴于现在的设备和浏览器运行得越来越快,这种将代码运行固定次数的测试方法有很大概念会得到一个0ms的时间差结果,显然0是毫无意义的。

方案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 → 单位时间(1秒)内进行的运算量
hz = 1 / period; // 上面两步可以简写如下:
// hz = (runs * 1000) / totalTime;

将测试代码一直循环运行直到总耗时totalTime 大于等于1000毫秒,也就是1秒种。

方案B 用于DromaeoV8 Benchmark Suite这两个库。

不足

由于有垃圾回收,(运行时的)引擎对代码的动态优化以及其他进程等的影响,此方案在重复进行测试时得到的结果不尽相同。为了得到更精确的测试结果,需要多次测试取均值。而上面提到的V8 库只会对测试运行一次,Dromaeo 则会运行5次,但其实还可以做得更彻底以获取更加精准的结果。一个可行的途径就是想办法将目前的测试时间由1000毫秒压缩到50毫秒,当然前提是系统提供给我们一个没有误差且绝对精确的时钟,这能保证时间尽可能多地用于运行测试代码(而不会过多地被操作系统的中间停顿浪费掉)。

方案 C

JSLitmus 这个库结合了前面两种方案的优点。采用方案A 来将测试代码运行n次,同时动态调整这个n值以保证测试能够进行到一个最小的时长,也就是方案B所描述的那样。

症结

JSLitmus 规避了方案A的缺点但同时引入了方案B的不足之处。为了进一步提高测试的准确率,JSLitmus 将结果进行了量化,取出3次空测试(译注:不太理解这里的空测试为何物,不挂测试代码空跑??)中运行最快的一次,再将每次基准测试的结果减去这个最快值。不幸的是这种做法为了规避B方案的毛病(译注:B方案需要运行多次以得到更多采样集合以取均值,换句话说要得到越准确的结果就要耗费越多的时间)反而使结果更不可靠了,因为取3次中最快的一次本身就不符合统计规律(译注:按统计学的做法,为了得到3次中最快的一次结果,这里又需要运行另外的测试来拿到一个所谓的最快的结果的集合,然后从中求均值)。尽管JSLitmus可以多次运行这样的基准测试,将量化后的均值与每次测试结果的均值进行差额运算,但这样得到的最终结果其身上的误差已经足够掩盖之前我们为了提高准确率而做的任何努力了。

方案 D

前面三种方案的短肋可以通过方法转编(function compilation 编译转化之意)和循环展开(loop unrolling)。

function test() {
x == y;
} while (iterations--) {
test();
} // …将会编译转化为 →
var hz,
startTime = new Date; x == y;
x == y;
x == y;
x == y;
x == y;
// … hz = (runs * 1000) / (new Date - startTime);

这种做法将测试代码变成了展开的形式,避免了循环和量化工作(译注:没有了循环也就无需统计单位时间内的运算量了)。

问题

然而,纵然如此它还是有不足之处的。将函数转编会消耗大量内存同时也把CPU拖慢。当你把一个测试跑上几百万次时,可以想象到会创建大量的字符串和转编无数的函数。

这还不算,因为一个函数完全有可能在遇到return后提前结束执行。所以如果测试中函数在第3行就返回了,将循环展开成上百万的代码就显得毫无意义。看来检测这些可能的提前退出还是很有必要的,然后回归到使用while语句(也就是方案A的做法)加上对循环结果的量化。

函数体的提取

在Benchmark.js的实现中,使用了一个稍微不同的做法。你可以认为它结合了方案A,B,C还有D的长处。考虑到内存因素,我们没有将循环展开。为了控制住增大结果误差的因素,同时又让测试代码可以使用较为自然的实现和变量,我们将每个测试代码的函数体提取出来。譬如,当用下面的代码进行测试时:

var x = 1,
y = '1'; function test() {
x == y;
} while (iterations--) {
test();
} // …转会转编为 → var x = 1,
y = '1';
while (iterations--) {
x == y;
}

如此一来,Benchmark.js 使用一个与 JSLitmus近似的技术:将提取出来的函数体放到一个循环中(这是方案A的做法),重复执行直到达到一个最小的时限(这是方案B),最后重复整个流程取一个严格意义上的统计均值作为结果。

注意事项

有偏差的毫秒时钟

某些浏览器与操作系统的组合中,由于种种因素存在时钟不准的情况。

例如:

Windows XP开机后,程序执行的时钟周期为 10毫秒,这在其他操作系统中一般为15毫秒。意思就是每隔10毫秒操作系统会接收到来自硬件(译注:也就是CPU的时钟系统)的一次中断。

一些很老的浏览器(IE或者火狐2)严重依赖操作系统的时钟,也就是说每次你调用new Date().getTime()它其实直接从系统那里去拿这个时间。很显然,如果内部系统的时间都只间隔10毫秒或者15毫秒才更新一次,那测试结果会受很大影响,准确性大大降低。这个问题是需要解决的。

值得庆幸的是,JavaScript是可以拿到最小的时间度量单位的。这之后,我们可以通过数学方式将测试结果的不确定性降低到只有1%。为此,我们将这个最小时间度量单位除以2以得到这个不确定性的值。假设我们在XP上用IE6,此种情况下最小的度量单位是15毫秒。这个不确定性的值就为15ms/2=7.5ms。然后我们想控制结果的误差到1%,于是乎我们将刚才得到的不确定性值除以0.01,就得到了达到测试要求需要的最小测试时限为:7.5/0.01=750ms

备选时钟

当启用--enable-benchmarking 标志后,Chrome和Chromium会暴露出一个叫做chrome.Interval的方法,可以用它作为一个高精度的时钟。

在编写Benchmark.js库时, John-David Dalton 经过一番折腾后将Java里这个纳秒级的时钟通过一个小的Java applet插件暴露到了JavaScript中。

使用更高精度的时钟可以缩短测试周期,相应地可以跑更多测试样本,从而得到一个误差更小的测试结果。

Firebug 会禁用火狐的 JIT

启用Firebug后会禁用火狐高性能的实时(just-in-time JIT)本地代码编译,然后你的代码会跑在普通的JavaScript解释器里面。这将会比原先慢很多。所以在跑基准测试时千万记得关掉Firebug。

其他浏览器的元素审查工具比如WebKit的Web Inspector或者欧朋浏览器的Dragonfly在开启时也有类似问题,尽管相比于上面的情况会小很多。所以在跑测试时最好还是关掉这些,或多或少还是会影响测试结果的。

浏览器缺陷和特性

基准测试内部实现中的一些循环机制容易受到一些浏览器本身缺陷的影响,比如像最近IE9的dead-code-removal展示的那样。火狐TraceMonkey 引擎的一个bug,还有欧朋11 querySelectorAll结果的缓存都会影响到测试结果。这些都是在进行测试是需要注意的。

统计学的重要性

大多数的基准测试/测试代码给出的结果并且没有严格符合统计学要求。John Resig(译注:jQuery原始作者)在他早前的一篇文章「JavaScript 基准测试的质量」中有提到。简单来说,就是应该尽量考虑到每个测试结果的误差并去减小它。扩大测试的样本值,健全的测试执行,都能够起到减少误差的作用。

跨浏览器的测试

如果你想在不同浏览器中进行测试且想得到较可靠的结果,一定要在真实的浏览器中测试。不要依赖于IE自带的兼容模式,此模式跟他所模拟的版本是存在实质性差异的。

还有就是除了跟大多其他浏览器一样会限制脚本的时间,IE(8及以下)还限制了代码的指令数不能超过5百万。事实上以现在CPU的吞吐能力,这样的数量级处理起来只是半秒钟的事情。如果你配置确实过硬,跑起来倒也没什么只是IE会给出一个Script Warning的警告,这种情况下你可以通过修改注册表来增大这个数量限制。幸运的是微软还提供了一个修复助手的程序,你只需要运行即可,比修改注册表方便多了。更可喜的是,IE9以上,这个逗逼的限制被移除了。

总结

无论你只是跑了一些测试,或者写一些用例,抑或正在自己写一个基准测试库,关于JavaScript基准测试的奥义远比你看到得要多(译注:就是水很深,并不是跑个分那么简单)。Benchmark.js和jsPerf每周都有更新,包含bug修复,新功能添加和一些提升准确率的技巧。但愿主流浏览器也能够为此做些努力吧...

JavaScript的基准测试-不服跑个分?的更多相关文章

  1. 不服跑个分:ARM鲲鹏云服务器实战评测——华为云鲲鹏KC1实例 vs. 阿里云G5实例【华为云技术分享】

    原文链接:https://m.ithome.com/html/444828.htm 今年一月份,华为正式发布了鲲鹏920数据中心高性能处理器,该处理器兼容ARM架构,采用7纳米制造,最高支持64核,主 ...

  2. JavaScript的基准测试

    JavaScript的基准测试 原文:Bulletproof JavaScript benchmarks 做JavaScript的基准测试并没有想的那么简单.即使不考虑浏览器差异所带来的影响,也有很多 ...

  3. JavaScript小实例-文字跑马灯效果

    我们常常能看到显示屏上字体的滚动以及手机弹幕等,下面所示代码就是一个简易的文字跑马灯的效果: <!DOCTYPE html> <html> <head lang=&quo ...

  4. javascript中setInterval制作跑马灯的效果

    html代码: javascript代码 <script type="text/javascript"> function scroll() { var title = ...

  5. .Net判断一个对象是否为数值类型探讨总结(高营养含量,含最终代码及跑分)

    前一篇发出来后引发了积极的探讨,起到了抛砖引玉效果,感谢大家参与. 吐槽一下:这个问题比其看起来要难得多得多啊. 大家的讨论最终还是没有一个完全正确的答案,不过我根据讨论结果总结了一个差不多算是最终版 ...

  6. .NET的前世今生与将来

    笔者注 谨以此文纪念我敬重的2016年9月17日去世的 装配脑袋 逝世两周年 让大家久等了,前后花了1年的时间,几经改版,终于完成撰写了一万字长文,回顾和展望.NET这16年来的成功与失败.最终能成文 ...

  7. python 时间合集 一

    **以下内容均为我个人的理解,如果发现错误或者疑问可以联系我共同探讨**#### python中4种时间表示形式:1.格式化时间字符串 2.时间戳 3.时间元祖 4.时间对象- string_time ...

  8. Azure 进阶攻略 | 电脑跑分你会,但虚拟机存储性能跑分的正确姿势你造吗?

    想学生时代,小编最爱做的就是研究电脑硬件,然后给自己.朋友和童鞋装机.装好后呢?当然要第一时间跑分了!各种跑分软件运行一遍,不断优化,不断测试.终于得到一个满意成绩,截图分享到网上显摆一下.当年为啥就 ...

  9. JavaScript基础知识点

    本书目录 第一章:  JavaScript语言基础 第二章:  JavaScript内置对象第三章:  窗口window对象第四章:  文档document对象第五章:  表单form对象第六章:   ...

随机推荐

  1. NodeJs在Linux下使用的各种问题

    环境:ubuntu16.04 ubuntu中安装NodeJs 通过apt-get命令安装后发现只能使用nodejs,而没有node命令 如果想避免这种情况请看下面连接的这种安装方式: 拓展见:Linu ...

  2. C语言 · 矩形面积交

    问题描述 平面上有两个矩形,它们的边平行于直角坐标系的X轴或Y轴.对于每个矩形,我们给出它的一对相对顶点的坐标,请你编程算出两个矩形的交的面积. 输入格式 输入仅包含两行,每行描述一个矩形. 在每行中 ...

  3. Android 获取meta-data中的数据

    在 Android 的 Mainfest 清单文件中,Application,Activity,Recriver,Service 的节点中都有这个的存在.很多时候我们可以通过 meta-data 来配 ...

  4. centos7+mono4+jexus5.6.2安装过程中的遇到的问题

    过程参考: http://www.linuxdot.net/ http://www.jexus.org/ http://www.mono-project.com/docs/getting-starte ...

  5. ASP.NET MVC5+EF6+EasyUI 后台管理系统(55)-Web打印

    系列目录 前言 1.本次主要弥补工作流,用户表单数据的打印 2.使用JQprint做为web打印插件 3.兼容:FireFox,Chrome,IE. 4.没有依赖也没有配置,使用简单 代码下载:htt ...

  6. [BootStrap] 富编辑器,基于wysihtml5

    在我的周围,已经有很多人在使用BootStrap,但对于任何一个带留言.评论.提问.文章编辑功的网站,编辑器永远是重中之重,显然,早期的编辑器完全没考虑过BootStrap的出现,或皮肤跟网站不匹配, ...

  7. RSA非对称加密,使用OpenSSL生成证书,iOS加密,java解密

    最近换了一份工作,工作了大概一个多月了吧.差不多得有两个月没有更新博客了吧.在新公司自己写了一个iOS的比较通用的可以架构一个中型应用的不算是框架的一个结构,并已经投入使用.哈哈 说说文章标题的相关的 ...

  8. 深入.NET平台和C#编程总结大全

    对于初学者的你,等到你把这个看完之后就更清楚地认知.NET和C#编程了,好了废话不多说,开始吧!                                                     ...

  9. C#关于分页显示

    ---<PS:本人菜鸟,大手子还请高台贵手> 以下是我今天在做分页时所遇到的一个分页显示问题,使用拼写SQL的方式写的,同类型可参考哦~ ------------------------- ...

  10. C# 工厂模式+虚方法(接口、抽象方法)实现多态

    面向对象语言的三大特征之一就是多态,听起来多态比较抽象,简而言之就是同一行为针对不同对象得到不同的结果,同一对象,在不同的环境下得到不同的状态. 实例说明: 业务需求:实现一个打开文件的控制台程序的d ...