JavaScript的基准测试-不服跑个分?
做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 用于Dromaeo和V8 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的基准测试-不服跑个分?的更多相关文章
- 不服跑个分:ARM鲲鹏云服务器实战评测——华为云鲲鹏KC1实例 vs. 阿里云G5实例【华为云技术分享】
原文链接:https://m.ithome.com/html/444828.htm 今年一月份,华为正式发布了鲲鹏920数据中心高性能处理器,该处理器兼容ARM架构,采用7纳米制造,最高支持64核,主 ...
- JavaScript的基准测试
JavaScript的基准测试 原文:Bulletproof JavaScript benchmarks 做JavaScript的基准测试并没有想的那么简单.即使不考虑浏览器差异所带来的影响,也有很多 ...
- JavaScript小实例-文字跑马灯效果
我们常常能看到显示屏上字体的滚动以及手机弹幕等,下面所示代码就是一个简易的文字跑马灯的效果: <!DOCTYPE html> <html> <head lang=&quo ...
- javascript中setInterval制作跑马灯的效果
html代码: javascript代码 <script type="text/javascript"> function scroll() { var title = ...
- .Net判断一个对象是否为数值类型探讨总结(高营养含量,含最终代码及跑分)
前一篇发出来后引发了积极的探讨,起到了抛砖引玉效果,感谢大家参与. 吐槽一下:这个问题比其看起来要难得多得多啊. 大家的讨论最终还是没有一个完全正确的答案,不过我根据讨论结果总结了一个差不多算是最终版 ...
- .NET的前世今生与将来
笔者注 谨以此文纪念我敬重的2016年9月17日去世的 装配脑袋 逝世两周年 让大家久等了,前后花了1年的时间,几经改版,终于完成撰写了一万字长文,回顾和展望.NET这16年来的成功与失败.最终能成文 ...
- python 时间合集 一
**以下内容均为我个人的理解,如果发现错误或者疑问可以联系我共同探讨**#### python中4种时间表示形式:1.格式化时间字符串 2.时间戳 3.时间元祖 4.时间对象- string_time ...
- Azure 进阶攻略 | 电脑跑分你会,但虚拟机存储性能跑分的正确姿势你造吗?
想学生时代,小编最爱做的就是研究电脑硬件,然后给自己.朋友和童鞋装机.装好后呢?当然要第一时间跑分了!各种跑分软件运行一遍,不断优化,不断测试.终于得到一个满意成绩,截图分享到网上显摆一下.当年为啥就 ...
- JavaScript基础知识点
本书目录 第一章: JavaScript语言基础 第二章: JavaScript内置对象第三章: 窗口window对象第四章: 文档document对象第五章: 表单form对象第六章: ...
随机推荐
- Android请求网络共通类——Hi_博客 Android App 开发笔记
今天 ,来分享一下 ,一个博客App的开发过程,以前也没开发过这种类型App 的经验,求大神们轻点喷. 首先我们要创建一个Andriod 项目 因为要从网络请求数据所以我们先来一个请求网络的共通类. ...
- Javascript生成二维码(QR)
网络上已经有非常多的二维码编码和解码工具和代码,很多都是服务器端的,也就是说需要一台服务器才能提供二维码的生成.本着对服务器性能的考虑,这种小事情都让服务器去做,感觉对不住服务器,尤其是对于大流量的网 ...
- MVC5+EF6+MYSQl,使用codeFirst的数据迁移
之前本人在用MVC4+EF5+MYSQL搭建自己的博客.地址:www.seesharply.com;遇到一个问题,就是采用ef的codefirst模式来编写程序,我们一般会在程序开发初期直接在glob ...
- tomcat开发远程调试端口以及利用eclipse进行远程调试
一.tomcat开发远程调试端口 方法1 WIN系统 在catalina.bat里: SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compi ...
- 模拟AngularJS之依赖注入
一.概述 AngularJS有一经典之处就是依赖注入,对于什么是依赖注入,熟悉spring的同学应该都非常了解了,但,对于前端而言,还是比较新颖的. 依赖注入,简而言之,就是解除硬编码,达到解偶的目的 ...
- CRL快速开发框架系列教程三(更新数据)
本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...
- geotrellis使用(二十八)栅格数据色彩渲染(多波段真彩色)
目录 前言 实现过程 总结 一.前言 上一篇文章介绍了如何使用Geotrellis渲染单波段的栅格数据,已然很是头疼,这几天不懈努力之后工作又进了一步,整清楚了如何使用Geotrelli ...
- C#创建、安装、卸载、调试Windows Service(Windows 服务)的简单教程
前言:Microsoft Windows 服务能够创建在它们自己的 Windows 会话中可长时间运行的可执行应用程序.这些服务可以在计算机启动时自动启动,可以暂停和重新启动而且不显示任何用户界面.这 ...
- 浅谈Java的throw与throws
转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...
- JavaScript求两个数字之间所有数字的和
这是在fcc上的中级算法中的第一题,拉出来的原因并不是因为有什么好说的,而是我刚看时以为是求两个数字的和, 很显然错了.我感觉自己的文字理解能力被严重鄙视了- -.故拉出来折腾折腾. 要求: 给你一个 ...