Implémenter Fast Inverse Square Root en Javascript?

11

La racine carrée inverse rapide de Quake III semble utiliser une astuce en virgule flottante. Si je comprends bien, la représentation en virgule flottante peut avoir différentes implémentations.

Est-il donc possible d'implémenter la racine carrée inverse rapide en Javascript?

Renverrait-il le même résultat?

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
la source
Faites-moi savoir si cette question serait mieux posée sur StackOverflow. Cela semblait plus approprié ici car il a des racines de développement de jeu et principalement des applications de développement de jeu.
Atav32
4
Javascript a des pointeurs?
Pubby
2
Bien qu'il soit tentant d'utiliser une fonction "spéciale" qui accélère l'ensemble de votre programme, il est probable que vous introduisiez des bogues ou que vous n'accélériez tout simplement pas du tout (voir la réponse de Kevin Reid ci-dessous par exemple). c2.com/cgi/wiki?PrematureOptimization
Daniel Carlsson
Je suis désolé, mais l'utilisation d'optimisations de FP de bas niveau avec Javascript revient à commander 4 gros hamburgers avec des frites et un cola diététique pour rester mince. Ne faites pas ça, c'est inutile et ridicule.
Nevermind
Le sqrt inverse rapide est une opération très courante dans la programmation de jeux, et toutes les consoles de jeu l'implémentent dans le matériel. ES6 devrait certainement envisager d'ajouter Math.fastinvsqrt (x) au langage.
John Henckel

Réponses:

15

L'astuce dépend de la réinterprétation des bits d'un nombre à virgule flottante en tant qu'entiers et inversement, ce qui est possible en JavaScript en utilisant la fonction Tableaux typés , pour créer un tampon d'octets bruts avec plusieurs vues numériques.

Voici une conversion littérale du code que vous avez donné; notez que ce n'est pas exactement la même chose, car toutes les opérations arithmétiques en JavaScript sont en virgule flottante 64 bits, et non 32 bits, donc l'entrée sera nécessairement convertie. En outre, comme le code d'origine, cela dépend de la plate-forme en ce qu'il donnera des résultats non-sens si l'architecture du processeur utilise un ordre d'octets différent; si vous devez faire des choses comme ça, je recommande que votre application exécute d'abord un cas de test pour déterminer que les entiers et les flottants ont les représentations d'octets que vous attendez.

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

J'ai confirmé en observant un graphique que cela donne des résultats numériques raisonnables. Cependant, il n'est pas évident que cela améliorera les performances, car nous effectuons davantage d'opérations JavaScript de haut niveau. J'ai exécuté des tests de performance sur les navigateurs que j'ai à portée de main et j'ai constaté que cela Q_rsqrt(number)prenait 50% à 80% du temps pris par 1/sqrt(number)(Chrome, Firefox et Safari sur macOS, à partir d'avril 2018). Voici ma configuration de test complète:

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
la source
In classic JavaScript, it is not possible to... reinterpreting the bits of a floating-point number as an integervraiment? C'était il y a des années, donc je ne me souviens pas exactement des opérations que j'utilisais, mais j'ai déjà écrit un analyseur de données en JavaScript qui convertirait une chaîne d'octets en une série d'entiers de N bits (N était défini dans l'en-tête). C'est assez similaire.
jhocking