JS取整方法和性能测试

这篇文章的由来是因为之前的帖子:《【JS时间戳】获取时间戳的最快方式探究》

这里将其取整计算的部分单独拿出来再水一篇!以下测试都是基于NodeJS环境下的测试,浏览器下可能会略有出入?这里就不展开讨论了。

JS中取整方法有很多,考虑到性能方面的话,通常就是Math库和位运算了。当然通过转为字符串后处理的方式也是可以的,但是效率上就没有这两个数学运算的效率高了。所以今天我们只讨论这两种类型下的方法和效率。

首先看一下jsperf上在浏览器环境下的测试结果:

对比测试

下面我们用NodeJS中的performance来测试对比各种取整方法的效率,在测试代码前加入如下代码,准备好performance的测试环境:

// 引入perf_hooks库
const {
    performance,
    PerformanceObserver
} = require('perf_hooks')
// 新建一个PerformanceObserver,里面对各个结果进行打印
const obs = new PerformanceObserver((list, observer) => {
    list.getEntries().forEach(i => console.log(`${i.entryType} ${i.name} duration:${i.duration}`))
    // observer.disconnect() // 如果buffered=false,则请注释掉
    performance.clearMarks();
});
// 配置observer只关注measure、gc和function三类entryType,其他的忽略
// 关闭buffered模式(默认关闭),每次调用时输出,方便进行排查
obs.observe({
    entryTypes: ['gc', 'measure', 'function'],
    buffered: false
});
  • 通过Math库各取整方法及速度对比

我们设定测试的重复次数为1000000000次,分别测试Math库中的各种方法来实现取整,代码如下:

const interval = 1000000000
let tmp = 0,
    no = 0

// 生成一个随机数
const D = Math.random() * 10000
console.log('Begain To Test With:' + D)
console.log('========================================\n')

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.floor(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.trunc(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.ceil(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.round(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

对比结果如下,多测测试后得出大体结论,除了round四舍五入比较慢外,其他几种方式在interval模式下大致相同:

在测试中发现了一个问题,就是通过上面的代码进行标定测试的时候,虽然每次测试结果不尽相同,但是有一个神奇的规律:第一个执行的方法总是会多出一点儿时间,最后一个执行的方法时间总是会缩短一点儿。而且问题在于都没有触发gc,所以也不知道是什么原因。。。

Begain To Test With:796.8644837186533
========================================

1 796
measure 1 duration:1692.5223
2 796
measure 2 duration:1638.305501
3 797
measure 3 duration:1607.331799
4 797
measure 4 duration:2535.9533
  • 通过parseInt、字符串方式取整

同样设定测试的重复次数为1000000000次,分别测试parseInt和字符串处理的各种方法来实现取整,代码如下:

const interval = 1000000000
let tmp = 0,
    no = 0

// 生成一个随机数
const D = Math.random() * 10000
console.log('Begain To Test With:' + D)
console.log('========================================\n')


performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.trunc(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = parseInt(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = parseInt(D, 0)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

const d = `${D}`
performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = parseInt(d)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = d.split('.')[0]
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

const reg = /([0-9]*)\./
performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = reg.exec(d)[1]
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

为了对比最后加了一个Math.trunc,就测试了一次(不想再测了。。。),两次parseInt(number)速度差不多,但也很慢了,不过和parseInt(string)比起来快多了。相比之下split是最慢的,而正则匹配甚至比parseInt(string)更快。当然最后两种方法得出的结果是string,还要再转为int才行。

Begain To Test With:5241.148738653594
========================================

1 5241
measure 1 duration:1619.2122
2 5241
measure 2 duration:4296.2127
3 5241
measure 3 duration:4329.045301
4 5241
measure 4 duration:83201.2534
5 '5241'
measure 5 duration:226317.6352
6 '5241'
measure 6 duration:56234.119499
  • 通过位运算进行取整

更多位运算相关请移步《JS中的位运算》了解更多
通过位运算X|0,~~X,X^0,X>>0,X<<0都可以实现小数的取整

位运算的限制
位运算虽然性能高,但是存在一定的限制,某些情况下回导致精度丢失问题,负数问题等等。具体的原因可以参看我的另外一篇文章《JS位运算异常》

测试代码如下:


const interval = 1000000000
let tmp = 0,
    no = 0

// 生成一个随机数
const D = Math.random() * 10000
console.log('Begain To Test With:' + D)
console.log('========================================\n')


performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = D >> 0
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = D << 0
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = ~~D
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = D | 0
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

performance.mark(`Start ${no+=1}`)
for (let i = 0; i < interval; i++) {
    tmp = Math.trunc(D)
}
performance.mark(`End ${no}`)
console.log(no, tmp)
performance.measure(`${no}`, `Start ${no}`, `End ${no}`)

得到的结果:

Begain To Test With:5184.300411928617
========================================

1 5184
measure 1 duration:616.240701
2 5184
measure 2 duration:613.009399
3 5184
measure 3 duration:639.2851
4 5184
measure 4 duration:622.9319
5 5184
measure 5 duration:1272.4825

位运算的效率还是远远领先于Math库的

归纳总结

对于浮点数的取整,根据场景的不同,选择合适的方法可以达到事半功倍的效果。比如在确定数字取值范围不会超出32位整形-2147483648 到 2147483647的前提下,相较于Math.trunc来说,位运算有着绝对的性能优势。如果超出这个范围,位运算取整会带来异常。

如果需要上取整、下取整、四舍五入等运算,还是用Math库最划算

字符串的处理中通过正则可以提高效率。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。