Raiz quadrada inversa rápida incomum de John Carmack (Quake III)

112

John Carmack tem uma função especial no código-fonte do Quake III que calcula a raiz quadrada inversa de um float, 4x mais rápido que o normal (float)(1.0/sqrt(x)), incluindo uma 0x5f3759dfconstante estranha . Veja o código abaixo. Alguém pode explicar linha por linha o que exatamente está acontecendo aqui e por que isso funciona muito mais rápido do que a implementação 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
fonte
10
Isso foi escrito zilhões de vezes. Consulte: google.com/search?q=0x5f3759df
Greg Hewgill
15
Obrigado, entretanto. Essa era uma pergunta muito mais interessante do que "como tornar um número positivo negativo em C #?"
MusiGenesis
7
Caramba, este é apenas um hack baseado no método de Newton, não é um santo graal dos algoritmos, pare de falar sobre isso, apelos: P
ldog

Respostas:

75

PARA SUA INFORMAÇÃO. Carmack não o escreveu. Terje Mathisen e Gary Tarolli têm crédito parcial (e muito modesto) por ele, bem como algumas outras fontes.

Como a constante mítica foi derivada é um mistério.

Para citar Gary Tarolli:

Que na verdade está fazendo um cálculo de ponto flutuante em inteiro - demorou muito para descobrir como e por que isso funciona, e não consigo mais me lembrar dos detalhes.

Uma constante ligeiramente melhor, desenvolvida por um matemático especialista (Chris Lomont) tentando descobrir como o algoritmo original funcionava, é:

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

Apesar disso, sua tentativa inicial de uma versão matematicamente 'superior' do sqrt do id (que chegou a quase a mesma constante) provou ser inferior à inicialmente desenvolvida por Gary, apesar de ser matematicamente muito mais 'puro'. Ele não conseguia explicar por que o id era tão excelente iirc.

Rushyo
fonte
4
O que significa "matematicamente mais puro"?
Tara
1
Eu imaginaria onde a primeira suposição pode ser derivada de constantes justificáveis, em vez de ser aparentemente arbitrária. Embora se você quiser uma descrição técnica, você pode procurá-la. Não sou um matemático e uma discussão semântica sobre terminologia matemática não pertence ao SO.
Rushyo
7
Essa é exatamente a razão pela qual encapsulei essa palavra em aspas assustadoras, para evitar esse tipo de bobagem. Isso pressupõe que o leitor esteja familiarizado com a escrita coloquial em inglês, eu acho. Você pensaria que o bom senso seria suficiente. Não usei um termo vago porque pensei "quer saber, eu realmente quero ser questionado sobre isso por alguém que não se dá ao trabalho de procurar a fonte original, o que levaria dois segundos no Google".
Rushyo
2
Bem, você realmente não respondeu à pergunta.
BJovke
1
Para quem queria saber onde o encontra: beyond3d.com/content/articles/8
mr5
52

É claro que hoje em dia, acaba sendo muito mais lento do que apenas usar um sqrt de FPU (especialmente no 360 / PS3), porque a troca entre os registradores float e int induz um load-hit-store, enquanto a unidade de ponto flutuante pode fazer um quadrado recíproco root no hardware.

Isso apenas mostra como as otimizações precisam evoluir conforme a natureza das mudanças de hardware subjacentes.

Crashworks
fonte
4
Ainda é muito mais rápido do que std :: sqrt ().
Tara
2
Você tem uma fonte? Quero testar os tempos de execução, mas não tenho um kit de desenvolvimento do Xbox 360.
DucRP
31

Greg Hewgill e IllidanS4 deram um link com uma excelente explicação matemática. Vou tentar resumir aqui para aqueles que não querem entrar muito em detalhes.

Qualquer função matemática, com algumas exceções, pode ser representada por uma soma polinomial:

y = f(x)

pode ser exatamente transformado em:

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

Onde a0, a1, a2, ... são constantes . O problema é que para muitas funções, como raiz quadrada, para valor exato essa soma tem um número infinito de membros, ela não termina em algum x ^ n . Mas, se pararmos em algum x ^ n , ainda teremos um resultado com alguma precisão.

Então, se tivermos:

y = 1/sqrt(x)

Neste caso particular, eles decidiram descartar todos os membros polinomiais acima do segundo, provavelmente por causa da velocidade de cálculo:

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

E agora chegou a tarefa de calcular a0 e a1 para que y tenha a menor diferença do valor exato. Eles calcularam que os valores mais apropriados são:

a0 = 0x5f375a86
a1 = -0.5

Então, quando você coloca isso na equação, você obtém:

y = 0x5f375a86 - 0.5*x

Que é a mesma linha que você vê no código:

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

Edit: na verdade aqui y = 0x5f375a86 - 0.5*xnão é o mesmo que i = 0x5f375a86 - (i >> 1);mudar o float como inteiro não só divide por dois, mas também divide o expoente por dois e causa alguns outros artefatos, mas ainda se resume a calcular alguns coeficientes a0, a1, a2 ....

Nesse ponto, eles descobriram que a precisão desse resultado não é suficiente para o propósito. Então, eles também fizeram apenas uma etapa da iteração de Newton para melhorar a precisão do resultado:

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

Eles poderiam ter feito mais algumas iterações em um loop, cada uma melhorando o resultado, até que a precisão necessária seja alcançada. É exatamente assim que funciona na CPU / FPU! Mas parece que apenas uma iteração foi suficiente, o que também foi uma bênção para a velocidade. A CPU / FPU faz quantas iterações forem necessárias para atingir a precisão do número de ponto flutuante no qual o resultado é armazenado e tem um algoritmo mais geral que funciona para todos os casos.


Resumindo, o que eles fizeram é:

Use (quase) o mesmo algoritmo que CPU / FPU, explore a melhoria das condições iniciais para o caso especial de 1 / sqrt (x) e não calcule todo o caminho para a precisão CPU / FPU irá para, mas pare antes, portanto ganhando em velocidade de cálculo.

BJovke
fonte
2
Converter o ponteiro em long é uma aproximação de log_2 (float). Jogá-lo de volta tem aproximadamente 2 ^ de comprimento. Isso significa que você pode tornar a proporção aproximadamente linear.
wizzwizz4 01 de
22

De acordo com este belo artigo escrito há algum tempo ...

A mágica do código, mesmo que você não possa segui-lo, se destaca como i = 0x5f3759df - (i >> 1); linha. Simplificado, Newton-Raphson é uma aproximação que começa com uma suposição e a refina com iteração. Aproveitando a natureza dos processadores x86 de 32 bits, i, um inteiro, é inicialmente definido como o valor do número de ponto flutuante do qual você deseja obter o quadrado inverso, usando uma conversão de inteiro. i é então definido como 0x5f3759df, menos ele próprio deslocado um bit para a direita. O deslocamento para a direita diminui o bit menos significativo de i, essencialmente reduzindo-o à metade.

É uma leitura muito boa. Este é apenas um pequeno pedaço dela.

Dillie-O
fonte
19

Eu estava curioso para ver o que era a constante como um float, então simplesmente escrevi este trecho de código e pesquisei o número inteiro que apareceu.

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

Parece que a constante é "Uma aproximação de inteiro da raiz quadrada de 2 ^ 127, mais conhecida pela forma hexadecimal de sua representação de ponto flutuante, 0x5f3759df" https://mrob.com/pub/math/numbers-18.html

No mesmo site, ele explica tudo. https://mrob.com/pub/math/numbers-16.html#le009_16

ThisIsAReallyOldQuestion
fonte
6
Isso merece mais atenção. Tudo faz sentido depois de perceber que é apenas a raiz quadrada de 2 ^ 127 ...
u8y7541