Wdrożenie Fast Inverse Square Root w Javascript?

11

Fast Inverse Square Root z Quake III wydaje się używać sztuczki zmiennoprzecinkowej. Jak rozumiem, reprezentacja zmiennoprzecinkowa może mieć kilka różnych implementacji.

Czy można zaimplementować Fast Inverse Square Root w Javascript?

Czy zwróci ten sam wynik?

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
źródło
Daj mi znać, czy lepiej byłoby zadać to pytanie na StackOverflow. Wydawało się to bardziej odpowiednie, ponieważ ma korzenie dla twórców gier i głównie aplikacje dla twórców gier.
Atav32,
4
JavaScript ma wskaźniki?
Pubby
2
Chociaż kuszące jest użycie „specjalnej” funkcji, która przyspiesza cały program, istnieje szansa, że ​​wprowadzasz błędy lub po prostu wcale nie przyspieszasz (na przykład patrz odpowiedź Kevina Reida). c2.com/cgi/wiki?PrematureOptimization
Daniel Carlsson
Przykro mi, ale korzystanie z niskopoziomowych optymalizacji FP z Javascriptem wygląda jak zamawianie 4 grubych burgerów z frytkami i dietetycznej coli, aby pozostać szczupłym. Nie rób tego, to bezcelowe i śmieszne.
Nevermind,
Szybki odwrotny sqrt jest bardzo częstą operacją w programowaniu gier, a wszystkie konsole do gier implementują to w sprzęcie. ES6 zdecydowanie powinien rozważyć dodanie Math.fastinvsqrt (x) do języka.
John Henckel

Odpowiedzi:

15

Trik polega na ponownej interpretacji bitów liczb zmiennoprzecinkowych jako liczb całkowitych iz powrotem, co jest możliwe w JavaScript przy użyciu narzędzia Typed Arrays , w celu utworzenia bufora surowych bajtów z wieloma widokami numerycznymi.

Oto dosłowna konwersja podanego kodu; Zwróć uwagę, że nie jest dokładnie taki sam, ponieważ wszystkie operacje arytmetyczne w JavaScript są 64-bitowe zmiennoprzecinkowe, a nie 32-bitowe, więc dane wejściowe muszą zostać przekonwertowane. Podobnie jak oryginalny kod, jest to zależne od platformy , ponieważ daje nonsensowne wyniki, jeśli architektura procesora używa innej kolejności bajtów; jeśli musisz zrobić coś takiego, zalecamy, aby aplikacja najpierw wykonała test, aby ustalić, czy liczby całkowite i zmiennoprzecinkowe mają oczekiwaną reprezentację bajtów.

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

Potwierdziłem, patrząc na wykres, że daje to rozsądne wyniki liczbowe. Jednak nie jest oczywiste, że w ogóle poprawi to wydajność, ponieważ wykonujemy więcej operacji JavaScript na wysokim poziomie. Uruchomiłem testy porównawcze w przeglądarkach, które mam pod ręką i stwierdziłem, że Q_rsqrt(number)zajmuje to od 50% do 80% czasu 1/sqrt(number)(Chrome, Firefox i Safari na macOS, od kwietnia 2018 r.). Oto moja kompletna konfiguracja testowa:

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>

Kevin Reid
źródło
In classic JavaScript, it is not possible to... reinterpreting the bits of a floating-point number as an integernaprawdę? To było lata temu, więc nie pamiętam dokładnie, jakich operacji używałem, ale kiedyś napisałem parser danych w JavaScript, który przekształciłby ciąg bajtów w serię liczb całkowitych N-bitowych (N zostało zdefiniowane w nagłówku). To całkiem podobne.
jhocking