14

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 found a difference when the web page is viewed in Chrome vs Firefox.

In particular, on Firefox:

Math.log2(8) = 3

but now in Chrome:

Math.log2(8) = 2.9999999999999996

My JavaScript program was originally written to find the depth of the binary tree based on the number of elements as:

var tree_depth = Math.floor(Math.log2(n_elements)) + 1;

I made a simple modification to this formula so that it will still work correctly on Chrome:

var epsilon = 1.e-5;
var tree_depth = Math.floor(Math.log2(n_elements) + epsilon) + 1;

I have 2 questions:

  1. Has anyone else noticed a change in the precision in Chrome recently for Math.log2?

  2. Is there a more elegant modification than the one I made above by adding epsilon?

Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
Stefan Musarra
  • 1,429
  • 14
  • 16
  • 4
    It's a change in V8 actually. And yes, it [has been noticed](https://code.google.com/p/v8/issues/detail?id=3579). ) – raina77ow Oct 26 '14 at 07:07
  • 1
    What really amazes me is that a well-known shim (`Math.log2 = function(x) { return Math.log(x) / Math.LN2 };)` actually returns correct results for the precise powers in Chrome. – raina77ow Oct 26 '14 at 07:12
  • Thanks - I think that shim is definitely more elegant than adding epsilon. – Stefan Musarra Oct 26 '14 at 07:17
  • Actually, with `1 << 29` it works fine (because of Math.floor). – raina77ow Oct 26 '14 at 07:21
  • @raina77ow: hmm, interesting. Chrome currently uses the inverse of `LN2` and multiplies that (`log(x) * LOG2E`), so it seems that the rounding error of this shim happens to be on the right side. – Qantas 94 Heavy Oct 26 '14 at 07:26
  • So basically, is 1 << 29 the same as setting epsilon = 1e-30? – Stefan Musarra Oct 26 '14 at 07:30
  • @StefanMusarra: no, that's not the same. `1 << x` is the bitwise equivalent of `Math.pow(2, x)` for 0 < x < 32. – Qantas 94 Heavy Oct 26 '14 at 07:31
  • Actually, they didn't change anything. The key difference between V8 and Firefox's implementation (which I think can be followed through [this issue's thread](https://bugzilla.mozilla.org/show_bug.cgi?id=717379)) , it seems, is that the latter use cache of results. – raina77ow Oct 26 '14 at 07:32
  • 1
    Couldn't you just use `Math.round` instead of `Math.floor`? – soktinpk Oct 26 '14 at 12:35
  • 1
    @soktinpk: the OP looking for the depth of a binary tree, which doesn't round at the halfway point. – Qantas 94 Heavy Oct 26 '14 at 13:56
  • BTW, if there are 16 elements, isn't the depth of the tree meant to be 4, not 5? – Qantas 94 Heavy Oct 26 '14 at 13:57
  • @Qantas94Heavy What do you mean it doesn't round at the halfway point? The depth is meant to be an integer. There is no "halfway point". `Math.round` is just to correct errors (i.e. `Math.round(Math.log2(8)) // --> 3` or `Math.round(Math.log2(64)) // --> 6`. It's like setting `elipson = 0.5`. – soktinpk Oct 26 '14 at 14:16
  • @soktinpk: the number of elements in a binary tree does not have to be a power of two AFAIK. – Qantas 94 Heavy Oct 28 '14 at 02:09

2 Answers2

12

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

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; }

// 2^1024 rounds to Infinity
for (var i = 0; i < 1024; ++i) {
  var resultD = log2d(Math.pow(2, i));
  var resultM = log2m(Math.pow(2, i));

  if (resultD !== i) console.log('log2d: expected ' + i + ', actual ' + resultD);
  if (resultM !== i) console.log('log2m: expected ' + i + ', actual ' + resultM);
}

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

Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
  • Slightly related: http://jsperf.com/integer-log2/6 (all of them produce integer log2, in case that's not obvious) – copy Oct 26 '14 at 18:03
  • I opened a new blank instance of both Chrome and Firefox, went to developer tools console, and entered Math.log2(8). I still get 3 in Firefox and 2.9999999999999996 in Chrome, so I don't think I have a shim causing the difference. – Stefan Musarra Oct 26 '14 at 19:17
  • @StefanMusarra: sorry if I didn't make it clear. Sometimes people add code to support `Math.log2` in browsers that don't have the function yet, so they'll add something like `if (!Math.log2) Math.log2 = function (x) { return Math.log(x) / Math.LN2 };`. If you had previously tested this before Chrome added the `Math.log2` function, your results will now be different compared to before. – Qantas 94 Heavy Oct 26 '14 at 23:47
-1

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);
mildog8
  • 2,030
  • 2
  • 22
  • 36