内容简介:之前我们讨论过宏观层面上的如果我们要测试一段代码的运行速度(执行时间),我们通常第一时间会想到编写以下代码进行测试:这在很长一段时间里,我都认为这段代码能测试出绝大数多正确的结果,而事实上这段代码的结果非常不准确
之前我们讨论过宏观层面上的 JavaScript
性能问题,讨论了 asm.js
、 WebAssembly
和 WebWorker
技术,接下来我们探究一下 JavaScript
在微观层面上的性能问题,并逐步了解这些性能问题是否真实存在,以及是否需要花大量时间去优化。
性能测试问题
如果我们要测试一段代码的运行速度(执行时间),我们通常第一时间会想到编写以下代码进行测试:
var start = Date.now() // do something console.log('用时:' + (Date.now() - start)) 复制代码
这在很长一段时间里,我都认为这段代码能测试出绝大数多正确的结果,而事实上这段代码的结果非常不准确
- 它很有可能报告的时间是0,因为他的运行时间可能小于1ms。或者在一些早期引擎中,定时器的精度只有15ms,也就是这个运算至少要运行15ms才会有结果输出。
- 对于一个单次的运行几乎没有任何参考价值,我们不能保证引擎或系统在此刻没有受到其他因素干扰。
- 在获得时间戳时可能存在延迟。
- 不能确定引擎是否对这段测试代码进行了优化。在真实程序中引擎是否会同样优化这段代码,如果不能,这就会导致真实环境中代码运行变慢。
Benchmark.js
基于以上自写测试用例的弊端,我们首先需要做的是 重复
,简单的说,就是用循环把测试代码包起来,但这并不是一个简单的循环多次求平均值的过程,相关的考虑因素还有定时器精度,结果分布情况等。可靠的测试应该结合统计学的合理实践,所以在自己没有更好的解决方法之前,选用成熟的测试 工具 是一个正确的决定, Benchmark.js
就是一个这样的js库。
npm
方式安装 benchmark
npm i install --save 复制代码
编写一个测试文件
// index.js var Benchmark = require('benchmark'); function foo () { var arr = new Array(10000) for(var i = 0;i < arr.length;i++) { arr[i] = 0 } } var bench = new Benchmark( 'foo test', // 测试名 foo, // 测试内容 { setup: `console.log('start')`, // 每个测试循环开始时调用 teardown: `console.log('over')` // 每个测试循环结束时调用 } ) bench.run() // 开始测试 console.log(bench.hz) // 每秒运行数 console.log(bench.stats.moe) // 出错边界 console.log(bench.stats.variance) // 样本方差 复制代码
第三个参数中的 setup
和 teardown
是我们尤其要注意的,第三个参数指定测试用例的一些额外信息,其中的 setup
表示每个测试周期开始时执行的方法,可以只是方法体,也可以是指定方法, teardown
表示每个测试周期结束时执行的方法,类型同上。也就是运行上面的代码 setup
不止执行一次,具体执行次数由 Benchmark.prototype.circle
决定。
性能优化的注意点
性能优化是否存在真实意义
比如在一次测试环境中,测试运算A每秒可运行 10 000 000
次,运算B每秒可运行 8 000 000
,这只能在数学意义上来讲B比A慢了 20%
。
我们换个比较方法,从上面的结果不难推出A单次运行需要 100ns
,据说人眼通常能分辨 100ms
以下的事件,人脑可以处理的最快速度是 13ms
。也就是运算A要运行 650 000
次才能有希望被人类感知到,而在 web
应用中,几乎很少会进行类似的操作。
比较这么微小的差异和比较 ++a
a++
在性能上的差异一样,意义不大。
引擎优化
由于引擎优化的存在,所以你不能确定一个运算A是否始终比运算B快,下面的代码
var a = '12' // 测试1 var A = Number(a) // 测试2 var B = parseInt(a) 复制代码
这段代码想比较 Number
和 parseInt
在类型转换上的性能差异,但是由于引擎优化的存在,这种测试会变得没有参考性,由于引擎优化没有被纳入es的规范内容,可能有些引擎在运行测试代码的时候进行了启发式优化,它发现A和B都没有在后续被使用,所以在整个测试中实际上什么事情都没有发生,而在真实环境中,可能又并非如此。所以我们必须让测试环境更可能的接近真实环境。
很多情况下需要测试不同环境下的代码运行情况,比如在 chrome
和在手机版 chrome
中的结果对比,在满电手机和电量 2%
以下手机的运行结果对比。 jsPerf.com
是一个共享测试用例和测试结果的平台。
过早优化是万恶之源
程序员们浪费了大量的时间用于思考,或担心他们的程序中非关键部分的速度,这些针对效率的努力在调试和维护方面带来了强烈的负面效果。我们应该在,比如说97%的时间里,忘掉小处的效率:过早优化是万恶之源。但我们不应该错过关键的3%的机会。 《计算访谈6》
不应该在非关键部分花太多时间,比如你的应用是一个动画表现的应用,就应该重点优化动画循环相关的代码。
测试用例举例
// 测试1 var x = [1,2,3,4,5] x.sort() // 测试2 var x = [1,2,3,4,5] x.sort(function (a,b) { return a - b }) 复制代码
这两个测试对比 sort(..)
内建方法和自定义方法的性能,但是这创建的了一个不公平的对比:
- 在循序测试中,自定义方法会不断被创建,这显然会增加额外的开销。
-
忽略了内建方法的额外工作:内建方法是将比较值强制装换成字符串进行比较,比如内建 排序 会把
18
排在2
前面。
// 测试1 var x = false; var y = x ? 1 : 2; // 测试2 var x; var y = x ? 1 : 2; 复制代码
上面这个测试如果是想比较 Boolean
值强制类型转换对性能的影响,那么就创建了一个不公平的对比,因为测试2少做了 x
的赋值操作。要消除这个影响,应该这样做:
// 测试2 var x = undefined; var y = x ? 1 : 2; 复制代码
最后我们来实际测试一下,在 for
循环中是否需要预先将 arr.length
设定好
var Benchmark = require('benchmark'); var suite = new Benchmark.Suite; // Benchmark.Suite是用来比较多个测试用例的类 var arr = new Array(1000) suite.add('len', function () { // 添加一个测试用例 for (var i = 0; i < arr.length; i++) { arr[i] = 1 } }, { setup: function () { arr = new Array(1000) } }) .add('preLen', function () { for (var i = 0, len = arr.length; i < len; i++) { arr[i] = 1 } }, { setup: function () { arr = new Array(1000) } }) .run() console.log(suite[0].hz) console.log(suite[1].hz) // 1160748.8603394227 // 1188525.8945115102 // 1182959.0564495493 // 1167161.734632912 // 1196721.6273367293 // 1195146.3296931305 复制代码
以上代码的测试环境为 nodejs@v8.11.4
,测试结果可以看出将 arr.length
提前保存反而会造成反优化,其实背后的原因就是在 v8
等现代 JavaScript
引擎中对这种循环已经做过优化,不会在每次循环都会去访问 arr.length
,所以开发者不再需要考虑这方面的问题,不要想在这方面能比引擎更聪明,结果只会适得其反。
尾调用优化
es规范通常不会涉及性能方面的要求,但 es6
中有一个例外,那就是尾调用优化( Tail Call Optimization
, TCO
),简单的说,尾调用就是在一个函数的末尾进行的函数调用。
在递归中,尾调用优化可能起到非常重要的作用
// 非尾调用 function foo () { foo() } // 非尾调用 function foo () { return 1 + foo() } // 尾调用 function foo () { return foo() } 复制代码
调用一个新的函数需要额外预留一块内存来管理调用帧,称为 栈帧
,在没有 TCO
的递归调用中,递归层级太多会导致栈溢出,递归无法运行。而在支持 TCO
的环境并正确书写 TCO
规范的递归函数,第二层的递归函数中直接使用上层函数的栈帧,依次类推。这样不仅速度快,也更节省内存。
作者简介:叶茂,芦苇科技web前端开发工程师,代表作品:口红挑战网红小游戏、芦苇科技官网。擅长网站建设、公众号开发、微信小程序开发、小游戏、公众号开发,专注于前端框架、服务端渲染、SEO技术、交互设计、图像绘制、数据分析等研究。
欢迎和我们一起并肩作战:web@talkmoney.cn 访问www.talkmoney.cn 了解更多
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 微服务测试之性能测试
- Go 单元测试和性能测试
- 性能测试vs压力测试vs负载测试
- SpringBoot | 第十三章:测试相关(单元测试、性能测试)
- Golang 性能测试 (2) 性能分析
- 随行付微服务测试之性能测试 原 荐
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。