Raíz cuadrada inversa rápida inusual de John Carmack (Quake III)

112

John Carmack tiene una función especial en el código fuente de Quake III que calcula la raíz cuadrada inversa de un flotador, 4 veces más rápido que lo normal (float)(1.0/sqrt(x)), incluyendo una 0x5f3759dfconstante extraña . Vea el código a continuación. ¿Alguien puede explicar línea por línea qué está sucediendo exactamente aquí y por qué funciona mucho más rápido que la implementación normal?

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

  #ifndef Q3_VM
  #ifdef __linux__
    assert( !isnan(y) );
  #endif
  #endif
  return y;
}
Alex
fuente
10
Esto se ha escrito miles de millones de veces. Ver: google.com/search?q=0x5f3759df
Greg Hewgill
15
Pero gracias. Esta fue una pregunta mucho más interesante que "¿cómo se hace que un número positivo sea negativo en C #?"
MusiGenesis
7
Mierda, esto es solo un truco basado en el método de Newton, no es un santo grial de algoritmos, deja de hablar de eso
favor

Respuestas:

75

FYI. Carmack no lo escribió. Terje Mathisen y Gary Tarolli se atribuyen un mérito parcial (y muy modesto) por ello, además de acreditar algunas otras fuentes.

Cómo se derivó la constante mítica es algo misterioso.

Para citar a Gary Tarolli:

Lo que en realidad está haciendo un cálculo de punto flotante en números enteros: tomó mucho tiempo descubrir cómo y por qué funciona esto, y ya no puedo recordar los detalles.

Una constante ligeramente mejor, desarrollada por un matemático experto (Chris Lomont) que intenta averiguar cómo funcionaba el algoritmo original es:

float InvSqrt(float x)
{
    float xhalf = 0.5f * x;
    int i = *(int*)&x;              // get bits for floating value
    i = 0x5f375a86 - (i >> 1);      // gives initial guess y0
    x = *(float*)&i;                // convert bits back to float
    x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
    return x;
}

A pesar de esto, su intento inicial de una versión matemáticamente 'superior' de la raíz cuadrada de id (que llegó a casi la misma constante) resultó ser inferior a la desarrollada inicialmente por Gary a pesar de ser matemáticamente mucho más 'pura'. No podía explicar por qué id's era tan excelente iirc.

Rushyo
fuente
4
¿Qué se supone que significa "matemáticamente más puro"?
Tara
1
Me imagino dónde se puede derivar la primera suposición de constantes justificables, en lugar de ser aparentemente arbitrario. Aunque si quieres una descripción técnica, puedes buscarla. No soy un matemático, y una discusión semántica sobre terminología matemática no pertenece a SO.
Rushyo
7
Esa es exactamente la razón por la que encapsulé esa palabra entre comillas de miedo, para evitar este tipo de tonterías. Eso supone que el lector está familiarizado con la escritura coloquial en inglés, supongo. Pensarías que el sentido común sería suficiente. No utilicé un término vago porque pensé "sabes qué, realmente quiero que alguien que no se moleste en buscar la fuente original me pregunte sobre esto, lo que tomaría dos segundos en Google".
Rushyo
2
Bueno, en realidad no ha respondido a la pregunta.
BJovke
1
Para aquellos que querían saber dónde lo encuentra: beyond3d.com/content/articles/8
mr5
52

Por supuesto, en estos días, resulta ser mucho más lento que simplemente usar un sqrt de FPU (especialmente en 360 / PS3), porque el intercambio entre los registros float e int induce un load-hit-store, mientras que la unidad de punto flotante puede hacer un cuadrado recíproco root en hardware.

Simplemente muestra cómo las optimizaciones tienen que evolucionar a medida que cambia la naturaleza del hardware subyacente.

Crashworks
fuente
4
Sin embargo, sigue siendo mucho más rápido que std :: sqrt ().
Tara
2
Tienes una fuente? Quiero probar los tiempos de ejecución pero no tengo un kit de desarrollo de Xbox 360.
DucRP
31

Greg Hewgill e IllidanS4 dieron un vínculo con una excelente explicación matemática. Intentaré resumirlo aquí para aquellos que no quieran entrar demasiado en detalles.

Cualquier función matemática, con algunas excepciones, se puede representar mediante una suma polinomial:

y = f(x)

se puede transformar exactamente en:

y = a0 + a1*x + a2*(x^2) + a3*(x^3) + a4*(x^4) + ...

Donde a0, a1, a2, ... son constantes . El problema es que para muchas funciones, como raíz cuadrada, para el valor exacto, esta suma tiene un número infinito de miembros, no termina en algún x ^ n . Pero, si nos detenemos en algún x ^ n todavía tendríamos un resultado con cierta precisión.

Entonces, si tenemos:

y = 1/sqrt(x)

En este caso particular, decidieron descartar todos los miembros polinomiales por encima del segundo, probablemente debido a la velocidad de cálculo:

y = a0 + a1*x + [...discarded...]

Y ahora ha bajado la tarea de calcular a0 y a1 para que y tenga la menor diferencia con el valor exacto. Han calculado que los valores más adecuados son:

a0 = 0x5f375a86
a1 = -0.5

Entonces, cuando pones esto en la ecuación, obtienes:

y = 0x5f375a86 - 0.5*x

Que es lo mismo que la línea que ves en el código:

i = 0x5f375a86 - (i >> 1);

Editar: en realidad, aquí y = 0x5f375a86 - 0.5*xno es lo mismo que i = 0x5f375a86 - (i >> 1);ya que cambiar el flotador como entero no solo divide por dos sino que también divide el exponente por dos y causa algunos otros artefactos, pero aún así se reduce a calcular algunos coeficientes a0, a1, a2 ...

En este punto, han descubierto que la precisión de este resultado no es suficiente para el propósito. Así que, además, hicieron solo un paso de la iteración de Newton para mejorar la precisión del resultado:

x = x * (1.5f - xhalf * x * x)

Podrían haber hecho algunas iteraciones más en un ciclo, cada una mejorando el resultado, hasta que se alcance la precisión requerida. ¡Así es exactamente como funciona en CPU / FPU! Pero parece que solo una iteración fue suficiente, lo que también fue una bendición para la velocidad. CPU / FPU hace tantas iteraciones como sea necesario para alcanzar la precisión del número de punto flotante en el que se almacena el resultado y tiene un algoritmo más general que funciona para todos los casos.


Entonces, en resumen, lo que hicieron fue:

Use (casi) el mismo algoritmo que CPU / FPU, aproveche la mejora de las condiciones iniciales para el caso especial de 1 / sqrt (x) y no calcule todo el camino hasta la precisión a la que llegará la CPU / FPU, pero se detendrá antes, por lo tanto ganando velocidad de cálculo.

BJovke
fuente
2
Convertir el puntero en long es una aproximación de log_2 (float). Lanzarlo hacia atrás es una aproximación de 2 ^ de largo. Esto significa que puede hacer que la relación sea aproximadamente lineal.
wizzwizz4
22

Según este bonito artículo escrito hace un tiempo ...

La magia del código, incluso si no puede seguirlo, se destaca como i = 0x5f3759df - (i >> 1); línea. Newton-Raphson, simplificado, es una aproximación que comienza con una suposición y la refina con una iteración. Aprovechando la naturaleza de los procesadores x86 de 32 bits, i, un número entero, se establece inicialmente en el valor del número de punto flotante del que desea tomar el cuadrado inverso, utilizando una conversión de números enteros. A continuación, i se establece en 0x5f3759df, menos él mismo desplazado un bit a la derecha. El cambio a la derecha elimina el bit menos significativo de i, esencialmente reduciéndolo a la mitad.

Es una lectura realmente buena. Esto es solo una pequeña parte.

Dillie-O
fuente
19

Tenía curiosidad por ver cuál era la constante como flotante, así que simplemente escribí este fragmento de código y busqué en Google el número entero que apareció.

    long i = 0x5F3759DF;
    float* fp = (float*)&i;
    printf("(2^127)^(1/2) = %f\n", *fp);
    //Output
    //(2^127)^(1/2) = 13211836172961054720.000000

Parece que la constante es "Una aproximación entera a la raíz cuadrada de 2 ^ 127 mejor conocida por la forma hexadecimal de su representación de punto flotante, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

En el mismo sitio lo explica todo. https://mrob.com/pub/math/numbers-16.html#le009_16

ThisIsAReallyOldQuestion
fuente
6
Esto merece más atención. Todo tiene sentido después de darse cuenta de que es solo la raíz cuadrada de 2 ^ 127 ...
u8y7541