Реализовать быстрый обратный квадратный корень в Javascript?

11

Корень быстрого обратного квадрата из Quake III, похоже, использует трюк с плавающей точкой. Как я понимаю, представление с плавающей точкой может иметь несколько разных реализаций.

Так можно ли реализовать быстрый корень квадратного обратного в Javascript?

Вернет ли он тот же результат?

float Q_rsqrt(float number) {

  long i;
  float x2, y;
  const float threehalfs = 1.5F;

  x2 = number * 0.5F;
  y = number;
  i = * ( long * ) &y;
  i = 0x5f3759df - ( i >> 1 );
  y = * ( float * ) &i;
  y = y * ( threehalfs - ( x2 * y * y ) );

  return y;

}
Atav32
источник
Дайте мне знать, если этот вопрос лучше задать на StackOverflow. Здесь это показалось более уместным, поскольку у него есть корни для разработчиков игр и в основном приложения для разработчиков игр.
Atav32
4
У Javascript есть указатели?
Пабби
2
Хотя соблазнительно использовать «специальную» функцию, которая ускоряет всю вашу программу, есть вероятность, что вы вносите ошибки или просто не ускоряете события вообще (см., Например, ответ Кевина Рейда ниже). c2.com/cgi/wiki?PrematureOptimization
Даниэль Карлссон
Извините, но использование низкоуровневых оптимизаций FP с Javascript похоже на заказ 4 жирных бургеров с картошкой фри и диетической колой, чтобы оставаться тонким. Не делай этого, это бессмысленно и смешно.
Nevermind
Быстрый обратный sqrt - очень распространенная операция в программировании игр, и все игровые приставки реализуют это аппаратно. ES6 определенно следует рассмотреть возможность добавления Math.fastinvsqrt (x) к языку.
Джон Хенкель

Ответы:

15

Хитрость заключается в том , чтобы заново интерпретировать биты числа с плавающей запятой как целое число и обратно, что возможно в JavaScript с помощью средства Typed Arrays для создания необработанного байтового буфера с несколькими числовыми представлениями.

Вот буквальное преобразование кода, который вы дали; обратите внимание, что это не совсем то же самое, поскольку все арифметические операции в JavaScript являются 64-разрядными с плавающей точкой, а не 32-разрядными, поэтому входные данные обязательно будут преобразованы. Также, как и в исходном коде, это зависит от платформы в том смысле, что оно даст бессмысленные результаты, если в архитектуре процессора используется другой порядок байтов; если вы должны сделать что-то подобное, я рекомендую вашему приложению сначала выполнить тестовый пример, чтобы определить, что целые числа и числа с плавающей запятой имеют ожидаемые вами байтовые представления.

const bytes = new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT);
const floatView = new Float32Array(bytes);
const intView = new Uint32Array(bytes);
const threehalfs = 1.5;

function Q_rsqrt(number) {
  const x2 = number * 0.5;
  floatView[0] = number;
  intView[0] = 0x5f3759df - ( intView[0] >> 1 );
  let y = floatView[0];
  y = y * ( threehalfs - ( x2 * y * y ) );

  return y;
}

Посмотрев на график, я подтвердил, что это дает разумные числовые результаты. Однако не очевидно, что это вообще улучшит производительность, так как мы выполняем более высокоуровневые операции JavaScript. Я провел тесты в браузерах, которые мне пригодились, и обнаружил, что это Q_rsqrt(number)занимает от 50% до 80% времени 1/sqrt(number)(Chrome, Firefox и Safari на macOS, по состоянию на апрель 2018 года). Вот моя полная тестовая настройка:

const {sqrt, min, max} = Math;

const bytes = new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT);
const floatView = new Float32Array(bytes);
const intView = new Uint32Array(bytes);
const threehalfs = 1.5;

function Q_rsqrt(number) {
  const x2 = number * 0.5;
  floatView[0] = number;
  intView[0] = 0x5f3759df - ( intView[0] >> 1 );
  let y = floatView[0];
  y = y * ( threehalfs - ( x2 * y * y ) );

  return y;
}

// benchmark
const junk = new Float32Array(1);
function time(f) {
  const t0 = Date.now();
  f();
  const t1 = Date.now();
  return t1 - t0;
}
const timenat = time(() => { 
  for (let i = 0; i < 5000000; i++) junk[0] = 1/sqrt(i)
});
const timeq = time(() => {
  for (let i = 0; i < 5000000; i++) junk[0] = Q_rsqrt(i);
});
document.getElementById("info").textContent =
  "Native square root: " + timenat + " ms\n" +
  "Q_rsqrt: " + timeq + " ms\n" +
  "Ratio Q/N: " + timeq/timenat;

// plot results
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
function plot(f) {
  ctx.beginPath();
  const mid = canvas.height / 2;
  for (let i = 0; i < canvas.width; i++) {
    const x_f = i / canvas.width * 10;
    const y_f = f(x_f);
    const y_px = min(canvas.height - 1, max(0, mid - y_f * mid / 5));
    ctx[i == 0 ? "moveTo" : "lineTo"](i, y_px);
  }
  ctx.stroke();
  ctx.closePath();
}
ctx.strokeStyle = "black";
plot(x => 1/sqrt(x));
ctx.strokeStyle = "yellow";
plot(x => Q_rsqrt(x));
<pre id="info"></pre>
<canvas width="300" height="300" id="canvas"
        style="border: 1px solid black;"></canvas>

Кевин Рид
источник
In classic JavaScript, it is not possible to... reinterpreting the bits of a floating-point number as an integerдействительно? Это было много лет назад, поэтому я не помню точно, какие операции я использовал, но однажды я написал в JavaScript анализатор данных, который преобразует строку байтов в последовательность N-битных (в заголовке было определено N). Это очень похоже.
Джоккинг