你不知道的JavaScript-性能测试和调优

栏目: 编程工具 · 发布时间: 6年前

内容简介:之前我们讨论过宏观层面上的如果我们要测试一段代码的运行速度(执行时间),我们通常第一时间会想到编写以下代码进行测试:这在很长一段时间里,我都认为这段代码能测试出绝大数多正确的结果,而事实上这段代码的结果非常不准确

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

性能测试问题

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

var start = Date.now()

// do something

console.log('用时:' + (Date.now() - start))

复制代码

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

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

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

第三个参数中的 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'

// 测试1
var A = Number(a)

// 测试2
var B = parseInt(a)
复制代码

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

jsPerf.com

很多情况下需要测试不同环境下的代码运行情况,比如在 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(..) 内建方法和自定义方法的性能,但是这创建的了一个不公平的对比:

  1. 在循序测试中,自定义方法会不断被创建,这显然会增加额外的开销。
  2. 忽略了内建方法的额外工作:内建方法是将比较值强制装换成字符串进行比较,比如内建 排序 会把 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 了解更多


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

An Introduction to Probability Theory and Its Applications

An Introduction to Probability Theory and Its Applications

William Feller / Wiley / 1991-1-1 / USD 120.00

Major changes in this edition include the substitution of probabilistic arguments for combinatorial artifices, and the addition of new sections on branching processes, Markov chains, and the De Moivre......一起来看看 《An Introduction to Probability Theory and Its Applications》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试