Math.log2 precision has changed in Chrome

后端 未结 2 652
栀梦
栀梦 2021-02-13 10:51

I\'ve written a JavaScript program that calculates the depth of a binary tree based on the number of elements. My program has been working fine for months, but recently I\'ve f

相关标签:
2条回答
  • 2021-02-13 11:02

    Note: Math.log2 hasn't actually changed since it's been implemented in V8. Maybe you remembered incorrectly or you had included a shim that happened to get the result correct for these special cases before Chrome included its own implementation of Math.log2.

    Also, it seems that you should be using Math.ceil(x) rather than Math.floor(x) + 1.

    How can I solve this?

    To avoid relying on Math.log or Math.log2 being accurate amongst different implementations of JavaScript (the algorithm used is implementation-defined), you can use bitwise operators if you have less than 232 elements in your binary tree. This obviously isn't the fastest way of doing this (this is only O(n)), but it's a relatively simple example:

    function log2floor(x) {
      // match the behaviour of Math.floor(Math.log2(x)), change it if you like
      if (x === 0) return -Infinity;
    
      for (var i = 0; i < 32; ++i) {
        if (x >>> i === 1) return i;
      }
    }
    
    console.log(log2floor(36) + 1); // 6
    

    How is Math.log2 currently implemented in different browsers?

    The current implementation in Chrome is inaccurate as they rely on multiplying the value of Math.log(x) by Math.LOG2E, making it susceptible to rounding error (source):

    // ES6 draft 09-27-13, section 20.2.2.22.
    function MathLog2(x) {
      return MathLog(x) * 1.442695040888963407;  // log2(x) = log(x)/log(2).
    }
    

    If you are running Firefox, it either uses the native log2 function (if present), or if not (e.g. on Windows), uses a similar implementation to Chrome (source).

    The only difference is that instead of multiplying, they divide by log(2) instead:

    #if !HAVE_LOG2
    double log2(double x)
    {
        return log(x) / M_LN2;
    }
    #endif
    
    double
    js::math_log2_impl(MathCache *cache, double x)
    {
        return cache->lookup(log2, x, MathCache::Log2);
    }
    
    double
    js::math_log2_uncached(double x)
    {
        return log2(x);
    }
    
    bool
    js::math_log2(JSContext *cx, unsigned argc, Value *vp)
    {
        return math_function<math_log2_impl>(cx, argc, vp);
    }
    

    All the other code is just to cache the results in a table, which Chrome does not do. This does not have any effect on the accuracy of Firefox's Math.log2 function.

    Multiplying or dividing: how much of a difference does it make?

    To test the difference between dividing by Math.LN2 and multiplying by Math.LOG2E, we can use the following test:

    function log2d(x) { return Math.log(x) / Math.LN2; }
    function log2m(x) { return Math.log(x) * Math.LOG2E; }
    
    var pow = Math.pow;
    
    // 2^1024 rounds to Infinity
    for (var i = 0; i < 1024; ++i) {
      var resultD = log2d(pow(2, i));
      var resultM = log2m(pow(2, i));
    
      if (resultD !== i) console.log('log2d: expected ' + i + ', actual ' + resultD);
      if (resultM !== i) console.log('log2m: expected ' + i + ', actual ' + resultM);
    }
    

    Please note that no matter which function you use, they still have floating point errors for certain values1. It just so happens that the floating point representation of log(2) is less than the actual value, resulting in a value higher than the actual value (while log2(e) is lower). This means that using log(2) will round down to the correct value for these special cases.

    1: log(pow(2, 29)) / log(2) === 29.000000000000004

    0 讨论(0)
  • 2021-02-13 11:02

    You could perhaps do this instead

    // Math.log2(n_elements) to 10 decimal places
    var tree_depth = Math.floor(Math.round(Math.log2(n_elements) * 10000000000) / 10000000000);
    
    0 讨论(0)
提交回复
热议问题