博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
你不知道的JavaScript——性能测试和调优
阅读量:5797 次
发布时间:2019-06-18

本文共 4567 字,大约阅读时间需要 15 分钟。

性能测试和调优

你不知道的JavaScript读书笔记

之前我们讨论过宏观层面上的JavaScript性能问题,讨论了asm.jsWebAssemblyWebWorker技术,接下来我们探究一下JavaScript在微观层面上的性能问题,并逐步了解这些性能问题是否真实存在,以及是否需要花大量时间去优化。

性能测试问题

如果我们要测试一段代码的运行速度(执行时间),我们通常第一时间会想到编写以下代码进行测试:

var start = Date.now()// do somethingconsole.log('用时:' + (Date.now() - start))复制代码

这在很长一段时间里,我都认为这段代码能测试出绝大数多正确的结果,而事实上这段代码的结果非常不准确

  1. 它很有可能报告的时间是0,因为他的运行时间可能小于1ms。或者在一些早期引擎中,定时器的精度只有15ms,也就是这个运算至少要运行15ms才会有结果输出。
  2. 对于一个单次的运行几乎没有任何参考价值,我们不能保证引擎或系统在此刻没有受到其他因素干扰。
  3. 在获得时间戳时可能存在延迟。
  4. 不能确定引擎是否对这段测试代码进行了优化。在真实程序中引擎是否会同样优化这段代码,如果不能,这就会导致真实环境中代码运行变慢。

Benchmark.js

基于以上自写测试用例的弊端,我们首先需要做的是重复,简单的说,就是用循环把测试代码包起来,但这并不是一个简单的循环多次求平均值的过程,相关的考虑因素还有定时器精度,结果分布情况等。可靠的测试应该结合统计学的合理实践,所以在自己没有更好的解决方法之前,选用成熟的测试工具是一个正确的决定,Benchmark.js就是一个这样的js库。

npm方式安装benchmark

npm install benchmark --save复制代码

编写一个测试文件

// index.jsvar 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) // 样本方差复制代码

第三个参数中的setupteardown是我们尤其要注意的,第三个参数指定测试用例的一些额外信息,其中的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'// 测试1var A = Number(a)// 测试2var B = parseInt(a)复制代码

这段代码想比较NumberparseInt在类型转换上的性能差异,但是由于引擎优化的存在,这种测试会变得没有参考性,由于引擎优化没有被纳入es的规范内容,可能有些引擎在运行测试代码的时候进行了启发式优化,它发现A和B都没有在后续被使用,所以在整个测试中实际上什么事情都没有发生,而在真实环境中,可能又并非如此。所以我们必须让测试环境更可能的接近真实环境。

很多情况下需要测试不同环境下的代码运行情况,比如在chrome和在手机版chrome中的结果对比,在满电手机和电量2%以下手机的运行结果对比。jsPerf.com是一个共享测试用例和测试结果的平台。

过早优化是万恶之源

程序员们浪费了大量的时间用于思考,或担心他们的程序中非关键部分的速度,这些针对效率的努力在调试和维护方面带来了强烈的负面效果。我们应该在,比如说97%的时间里,忘掉小处的效率:过早优化是万恶之源。但我们不应该错过关键的3%的机会。 《计算访谈6》

不应该在非关键部分花太多时间,比如你的应用是一个动画表现的应用,就应该重点优化动画循环相关的代码。

测试用例举例

// 测试1var x = [1,2,3,4,5]x.sort()// 测试2var x = [1,2,3,4,5]x.sort(function (a,b) {  return a - b})复制代码

这两个测试对比sort(..)内建方法和自定义方法的性能,但是这创建的了一个不公平的对比:

  1. 在循序测试中,自定义方法会不断被创建,这显然会增加额外的开销。
  2. 忽略了内建方法的额外工作:内建方法是将比较值强制装换成字符串进行比较,比如内建排序会把18排在2前面。
// 测试1var x = false;var y = x ? 1 : 2;// 测试2var x;var y = x ? 1 : 2;复制代码

上面这个测试如果是想比较Boolean值强制类型转换对性能的影响,那么就创建了一个不公平的对比,因为测试2少做了x的赋值操作。要消除这个影响,应该这样做:

// 测试2var 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规范的递归函数,第二层的递归函数中直接使用上层函数的栈帧,依次类推。这样不仅速度快,也更节省内存。

感谢评论区大佬的指正,TCO虽然是es6的一部分,但实质是个非常有争议的提案,主流浏览器几乎没有实现它,chrome实现过一段时间, ,这是一份支持尾调用优化的引擎列表,可以看到Safari@12支持尾调用优化,有兴趣的小伙伴可以去验证一下。 递归通常是堆栈溢出的“高发区”,我们可以将递归改为循环的方式避免使用递归

"use strict"var a = 0,    b = 0;function demo () {    a++    if (a < 100000) {        return demo()    }    return a}setTimeout(() => {    console.log('递归: ' + demo())},1000)function demo2 () {    b++    if (b < 100000) {        return function () {            return demo2()        }    }    return b}function runner (fn) {    let val = fn()    while (typeof val == 'function') {        val = val()    }    return val}setTimeout(() => {    console.log('循环:' + runner(demo2))})复制代码

我们可以使用nvm安装node@6.2.0使用--harmony_tailcalls参数体验尾调用优化 上面的代码运行结果

转载地址:http://vnsfx.baihongyu.com/

你可能感兴趣的文章
OneAPM挂牌新三板,续写ITOM新篇章
查看>>
机器人操作系统来到Windows
查看>>
QCon讲师对对碰——洪小军采访梁宇鹏:就是爱Golang
查看>>
spring-boot支持websocket
查看>>
菜鸟笔记(一) - Java常见的乱码问题
查看>>
移动web端自定义tap与模拟hover效果
查看>>
[译] Java 8 Nashorn 教程
查看>>
我理想中的前端工作流
查看>>
记一次Git异常操作:将多个repository合并到同一repository的同一分支
查看>>
CodeIgniter 3.0 新手捣鼓源码(一) base_url()
查看>>
Chrome 广告屏蔽功能不影响浏览器性能
查看>>
Node.js ORM 框架 Sequelize 重要更新 v5 发布
查看>>
CentOS 7.0 安装 MySQL
查看>>
ssh 免密码登录
查看>>
配置CPE作为PPPoE或PPPoA的客户端
查看>>
Redis进阶实践之九 独立封装的RedisClient客户端工具类
查看>>
System V和BSD的init程序小结
查看>>
jquery hide和show方法
查看>>
.Net组件程序设计之异步调用
查看>>
linux运维面试总结3
查看>>