这篇文章的由来是因为之前的帖子:《【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
库最划算
字符串的处理中通过正则可以提高效率。
网友评论