6.3 jsPerf.com
尽管在所有的 JavaScript 运行环境下,Benchmark.js 都可用于测试代码的性能,但有一点一定要强调,如果你想要得到可靠的测试结论的话,就需要在很多不同的环境(桌面浏览器、移动设备,等等)中测试汇集测试结果。
比如,针对同样的测试高端桌面机器的性能很可能和智能手机上 Chrome 移动设备完全不同。而电量充足的智能手机上的结果可能也和同一个智能手机但电量只有 2% 时完全不同,因为这时候设备将会开始关闭无线模块和处理器。
如果想要在不止一个环境下得出像“X 比 Y 快”这样的有意义的结论成立,那你需要在尽可能多的真实环境下进行实际测试。仅仅因为在 Chrome 上某个 X 运算比 Y 快并不意味着这在所有的浏览器中都成立。当然你可能还想要交叉引用多个浏览器上的测试运行结果,并有用户的图形展示。
有一个很棒的网站正是因这样的需求而诞生的,名为 jsPerf(http://jsperf.com )。它使用我们前面介绍的 Benchmark.js 库来运行统计上精确可靠的测试,并把测试结果放在一个公开可得的 URL 上,你可以把这个 URL 转发给别人。
每次测试运行的时候,测试结果就会被收集并持久化,累积的测试结果会被图形化,并展示到一个页面上以供查看。
在这个网站上创建测试的时候,开始需要先填写两个测试用例,但是你可以按需增添加任意多的测试。你还可以设定在每个测试循环开始时运行的 setup 代码,以及每个测试循环结束时运行的 teardown 代码。
可以通过一个技巧实现只用一个测试用例(如果需要测试单个方法的性能,而不需要对比的话),就是在首次创建的时候在第二个测试输入框填入占位符文字,然后编辑测试并把第二个测试清空,也就是删除了它。你总是可以在以后增加新的测试用例。
可以定义初始页面设置(导入库、定义辅助工具函数、声明变量,等等)。还有选项可以在需要的时候定义 setup 和 teardown 行为,参见 6.1.2 节。
完整性检查
jsPerf 是一个很好的资源,但认真分析的话,出于本章之前列出的多种原因,公开发布的测试中有大量是有缺陷或无意义的。
考虑:
// 用例1 var x = []; for (var i=0; i<10; i++) { x[i] = "x"; } // 用例2 var x = []; for (var i=0; i<10; i++) { x[x.length] = "x"; } // 用例3 var x = []; for (var i=0; i<10; i++) { x.push( "x" ); }
这个测试场景的一些需要思考的现象如下。
· 对开发者来说,极常见的情况是:把自己的循环放入测试用例,却忘了 Benchmark.js 已经实现了你所需的全部重复。非常有可能这些情况下的 for 循环完全是不必要的噪音。
· 每个测试用例中 x 的声明和初始化可能是不必要的。回忆一下之前的内容,如果 x = [] 放在 setup 代码中,它并不会在每个测试迭代之前实际运行,而是只在每轮测试之前运行一次。这意味着 x 将会持续增长到非常大,而不是 for 循环中暗示的大小——10 。
所以,其目的是为了确定测试只局限于 JavaScript 引擎如何处理小数组(大小为 10 )吗?目的可能是这样,而如果确实是的话,你必须考虑这是否过多关注了微秒的内部实现细节。
另一方面,测试的目的是否包含数组实际上增加到非常大之后的环境?与真实使用情况相比,JavaScript 处理大数组的行为是否适当和精确呢?
· 目的是否是找出 x.length 或 x.push(..) 对向数组 x 添加内容的操作的性能的影响有多大?好吧,这可能是有效的测试目标。但话说回来,push(..) 是一个函数调用,所以它当然要比 [..] 访问慢。可以证明,用例 1 和 2 要比用例 3 公平得多。
以下是另一个例子,展示了典型的不同类型对比的缺陷:
// 用例1 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort(); // 用例2 var x = ["John","Albert","Sue","Frank","Bob"]; x.sort( function mySort(a,b){ if (a < b) return -1; if (a > b) return 1; return 0; } );
这里,很明显测试目标是找出自定义的比较函数 mySort(..) 比内建默认比较函数慢多少。但是,通过把函数 mySort(..) 指定为在线函数表达式,你已经创建了一个不公平 / 虚假的测试。这里,第二个用例中测试的不只是用户自定义 JavaScript 函数,它还在每个迭代中创建了一个新的函数表达式。
如果运行一个类似的测试,但是将其更新为将创建一个在线函数表达式与使用预先定义好的函数对比,如果发现在线函数表达式创建版本要慢 2% ~ 20%,你会不会感到吃惊?!
除非你这个测试特意要考虑在线函数表达式创建的代价,否则更好更公平的测试就是将 mySort(..) 的声明放在页面 setup 中——不要把它放在测试 setup 中,因为那会在每一轮不必要地重新声明——只需要在测试用例中通过名字引用它:x.sort(mySort) 。
根据前面的例子,还有一个陷阱是隐式地给一个测试用例避免或添加额外的工作,从而导致“拿苹果与橘子对比”的场景:
// 用例1 var x = [12,-14,0,3,18,0,2.9]; x.sort(); // 用例2 var x = [12,-14,0,3,18,0,2.9]; x.sort( function mySort(a,b){ return a - b; } );
除了前面提到的在线函数表达式陷阱,第二个用例的 mySort(..) 可以工作。因为你给它提供的是数字,但如果是字符串的话就会失败。第一个用例不会抛出错误,但它的行为不同了,输出结果也不同!这应该很明显,但是两个测试用例产生不同的输出几乎肯定会使整个测试变得无效!
不过,在这种情况下,除了不同的输出之外,内建的比较函数 sort(..) 实际上做了 mySort() 没有做的额外工作,包括内建的那个把比较值强制类型转化为字符串并进行字典序比较。第一段代码结果为 [-14, 0, 0, 12, 18, 2.9, 3] ,而第二段代码结果(基于目标而言可能更精确)为 [-14, 0, 0, 2.9, 3, 12, 18] 。
所以,这个测试是不公平的,因为对于不同的用例,它并没有做完全相同的事情。你得到的任何结果都是虚假的。
同样的陷阱可能会更加不易察觉:
// 用例1 var x = false; var y = x ? 1 : 2; // 用例2 var x; var y = x ? 1 : 2;
这里,目的可能是测试对 Boolean 值进行强制类型转换对性能的冲击:如果 x 表达式并不是 Boolean 运算符,? : 就会进行强制类型转换(参见本书的“类型和语法”部分)。所以,你显然可以接受如下事实:第二个用例中有额外的类型转换工作要做。
那么不易察觉的问题是什么呢?在第一个用例中设定了 x 的值,而在另一个中则没有设定,所以实际上你在第一个用例中做了在第二个用例中没有做的事。要消除这个潜在的(虽然很小的)影响,可以试着这样:
// 用例1 var x = false; var y = x ? 1 : 2; // 用例2 var x = undefined; var y = x ? 1 : 2;
现在两种情况下都有赋值语句了。所以你想要测试的内容(有无对 x 的类型转换)很可能就更加精确地被独立出来并被测试到了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论