Точность Math.log2 изменилась в Chrome

Я написал программу JavaScript, которая вычисляет глубину двоичного дерева на основе количества элементов. Моя программа работает отлично в течение нескольких месяцев, но в последнее время я обнаружил разницу, когда веб-страницу просматривается в Chrome vs Firefox.

В частности, в Firefox:

Math.log2(8) = 3

но теперь в Chrome:

Math.log2(8) = 2.9999999999999996

Моя программа JavaScript была первоначально написана, чтобы найти глубину двоичного дерева, основанную на числе элементов, как:

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

Я сделал простую модификацию этой формулы, чтобы она по-прежнему работала корректно в Chrome:

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

У меня есть 2 вопроса:

  • Кто-нибудь еще заметил изменения в Chrome недавно для Math.log2?

  • Есть ли более элегантная модификация, чем та, которую я сделал выше, добавив epsilon?

Ответы

Ответ 1

Примечание. Math.log2 фактически не изменилось с момента его внедрения в V8. Возможно, вы неправильно вспомнили или включили прокладку, которая произошло, чтобы получить правильный результат для этих особых случаев до Chrome включала собственную реализацию Math.log2.

Кроме того, кажется, что вы должны использовать Math.ceil(x), а не Math.floor(x) + 1.

Как я могу это решить?

Чтобы не полагаться на Math.log или Math.log2, быть точным среди различных реализаций JavaScript (используемый алгоритм - это реализация), вы можете использовать побитовые операторы, если у вас меньше 2 32 элементов в вашем двоичном дереве. Это, очевидно, не самый быстрый способ сделать это (это только O (n)), но это относительно простой пример:

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

Как Math.log2 реализуется в настоящее время в разных браузерах?

Текущая реализация в Chrome неточна, поскольку они полагаются на умножение значения Math.log(x) на Math.LOG2E, что делает его восприимчивым к ошибке округления (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).
}

Если вы используете Firefox, он либо использует встроенную функцию log2 (если присутствует), либо если нет (например, в Windows), использует аналогичную реализацию в Chrome (источник).

Единственное отличие состоит в том, что вместо умножения они делятся на log(2):

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

Весь другой код - это просто кешировать результаты в таблице, которую Chrome не делает. Это не влияет на точность функции Firefox Math.log2.

Умножение или деление: какая разница?

Чтобы проверить разницу между делением на Math.LN2 и умножением на Math.LOG2E, мы можем использовать следующий тест:

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

Обратите внимание, что независимо от того, какую функцию вы используете, у них все еще есть ошибки с плавающей запятой для определенных значений 1. Так получилось, что представление с плавающей запятой log(2) меньше фактического значения, что приводит к значению выше фактического значения (тогда как log2(e) ниже). Это означает, что использование log(2) округляется до правильного значения для этих особых случаев.

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

Ответ 2

Возможно, вы можете сделать это вместо

// Math.log2(n_elements) to 10 decimal places
var tree_depth = Math.floor(Math.round(Math.log2(n_elements) * 10000000000) / 10000000000);