美文网首页
JS取整方法和性能测试

JS取整方法和性能测试

作者: 囍冯总囍 | 来源:发表于2020-08-10 18:47 被阅读0次

    这篇文章的由来是因为之前的帖子:《【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库最划算

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

    相关文章

      网友评论

          本文标题:JS取整方法和性能测试

          本文链接:https://www.haomeiwen.com/subject/aqafdktx.html