【问题标题】:Why does Math.abs take so much longer than Math.round?为什么 Math.abs 比 Math.round 花费这么多时间?
【发布时间】:2015-05-21 04:09:43
【问题描述】:

我知道 Math.abs() 和 Math.round() 是非常不同的函数,但我认为它们的效率相对相似。

console.time('round');
for (var i = 0; i < 100000; i++) {

  var a = Math.random() - 0.5;
  Math.round(a);
}
console.timeEnd('round');
console.time('abs');
for (var i = 0; i < 100000; i++) {

  var a = Math.random() - 0.5;
  Math.abs(a);
}
console.timeEnd('abs');

前面产生了这些结果: 回合:136.435ms 绝对:4777.983ms

谁能解释时间上的根本差异?

编辑:当我在这里运行代码片段时,我会得到更快的结果。大约 2 和 3 毫秒。为什么它会在不同的标签中获得如此高的倍数?

谢谢!

【问题讨论】:

    标签: javascript performance algorithm profiling profiler


    【解决方案1】:
    1. 我不确定您是否在衡量自己的想法

      随机数真的会因为缓存丢失而搞砸。此外,您还减去了 0.5,这也是 FPU 操作,它比 abs 本身要复杂得多。我不是 JAVA 编码器,但我希望 Math.abs(x) 是浮点数而不是整数运算(在 C/C++ 中,abs 是整数,fabs 是浮点数)。我会在你的循环之前创建带有随机数的数组,然后在你的循环中使用它

    2. abs

      abs 只是符号位测试 + 非零尾数测试。如果实施包含早午餐,那么这可能会严重减慢速度。幸运的是,float/double abs 实现不需要任何早午餐,您只需屏蔽符号位(因为尾数不是标准 IEEE 754 格式的 2'os 补码)。

    3. round

      round 测试尾数小数部分的 MSB 是否为 1,如果是,则应用整数增量。因此目标被位移为整数并提取 MSB 小数位。如果实现包含早午餐,那么这可能会严重减慢速度,但通常更快的是提取 MSB 小数位到进位标志并使用adc。这仍然需要比abs 更多的工作,所以它应该更慢。

    4. 那么为什么结果会这样呢?

      您的实施/平台是否使用 FPU 或软件仿真?在 FPU 上,两种操作的复杂性几乎相同(因为与 FPU 通信的开销通常比此类操作本身更大)。在仿真时,它取决于操作的实现和目标平台架构(管道、缓存控制...)

      我的猜测是:

      • abs 循环在缓存未命中时更糟
      • abs 的实现并没有达到应有的优化程度
      • round 由编译器优化,有时 round(x)=floor(a+0.5) 之前有 a-=0.5;,因为你不使用 a 其他任何东西,编译器可能会忽略 floor(random-0.5+0.5) 并直接使用 floor(random)

    [注释]

    您测得的 132 毫秒和 4.7 秒的时间对于您尝试过的硬件来说太大了?如今,对于常见的 PC 硬件和代码解释器,您编辑的时间要合理得多。你测量了超过 1 次吗?

    如果你在browser中尝试这个,那么它可能会被其背景中的任何东西减慢(比如从不同的页面截取或仍在下载一些东西......)操作系统也可以暂停执行但不会那么多

    【讨论】:

    • 最初,我在我的 PC 上的 Chrome 中运行它(不确定规格,但它相当强大),从那时起,我只是获得了更快的速度。我想它一定是被背景中的某些东西放慢了速度。
    【解决方案2】:

    不是答案,而是一些研究。

    V8 引擎源码 (https://chromium.googlesource.com/v8/v8.git) 我在src/math.js 中找到了以下实现

    Math.random

    function MathRound(x) {
      return %RoundNumber(TO_NUMBER_INLINE(x));
    }
    

    %RoundNumber 应指代src/runtime/runtime-maths.cc

    RUNTIME_FUNCTION(Runtime_RoundNumber) {
      HandleScope scope(isolate);
      DCHECK(args.length() == 1);
      CONVERT_NUMBER_ARG_HANDLE_CHECKED(input, 0);
      isolate->counters()->math_round()->Increment();
    
      if (!input->IsHeapNumber()) {
        DCHECK(input->IsSmi());
        return *input;
      }
    
      Handle<HeapNumber> number = Handle<HeapNumber>::cast(input);
    
      double value = number->value();
      int exponent = number->get_exponent();
      int sign = number->get_sign();
    
      if (exponent < -1) {
        // Number in range ]-0.5..0.5[. These always round to +/-zero.
        if (sign) return isolate->heap()->minus_zero_value();
        return Smi::FromInt(0);
      }
    
      // We compare with kSmiValueSize - 2 because (2^30 - 0.1) has exponent 29 and
      // should be rounded to 2^30, which is not smi (for 31-bit smis, similar
      // argument holds for 32-bit smis).
      if (!sign && exponent < kSmiValueSize - 2) {
        return Smi::FromInt(static_cast<int>(value + 0.5));
      }
    
      // If the magnitude is big enough, there's no place for fraction part. If we
      // try to add 0.5 to this number, 1.0 will be added instead.
      if (exponent >= 52) {
        return *number;
      }
    
      if (sign && value >= -0.5) return isolate->heap()->minus_zero_value();
    
      // Do not call NumberFromDouble() to avoid extra checks.
      return *isolate->factory()->NewNumber(Floor(value + 0.5));
    }
    

    数学.abs

    function MathAbs(x) {
      x = +x;
      return (x > 0) ? x : 0 - x;
    }
    

    Node.JS 使用 V8 引擎,Chrome 也是如此

    我的测试用例:

    var randoms = [];
    for (var i = 0; i < 100000; i++) {
        randoms.push(Math.random() - 0.5);
    }
    
    for(var r = 0; r < randoms.length; r++) {
        console.time('round');
        for (var i = 0; i < randoms.length; i++) {
            Math.round(randoms[i]);
        }
        console.timeEnd('round');
    
        console.time('abs');
            for (var i = 0; i < randoms.length; i++) {
            Math.abs(randoms[i]);
        }
        console.timeEnd('abs');  
    }
    

    结果:

    • Chrome (42.0.2311.152 m) - Math.random 更快
    • Node.JS (v0.10.29) - Math.abs 更快

    想法

    通过 V8 源,我希望 Math.abs 更快,它在 Node.JS 中,但不在 Chrome 中。

    想法为什么?

    【讨论】:

    • 您是否尝试过交换 abs 和 round 循环以检查您的测量是否不受 CACHE ... 的影响?还增加 N=100000 应该更安全......
    • @Spektre 左右交换,Node.JS 和 Chrome 的结果是一样的。
    • 相同是指交换前测量的相同还是现在不同浏览器之间没有区别?
    • @Spektre 与交换前测量的相同。我很高兴能提供图表和数字...
    • 在测量之前放弃测量受不同设置的缓存状态影响的可能性,这对循环之一不利。像这样Benchmarking affected by VCL
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-24
    • 2017-11-15
    • 1970-01-01
    • 2018-06-28
    • 1970-01-01
    • 1970-01-01
    • 2017-11-12
    相关资源
    最近更新 更多