A Incrível Raiz Quadrada Inversa
1.Introdução
O inverso da raiz quadrada é uma função presente em diversos problemas matemáticos, físicos e estatísticos. Aplicações algébricas de normalização, desvio padrão e até na indústria de videogames possuem freqüente utilização da simples expressão:
![]()
E foi na programação de um jogo que surgiu uma função que acabou por se tornar famosa entre os programadores por conseguir rodar até 4 vezes mais rápido que a implementação derivada da função sqrt(x), nativa da biblioteca do C.
A função que realizava o cálculo tão mais rapidamente com um erro associado de 1,75x10E-4 se apresentou muito eficiente na época e significou uma melhora considerável na execução do Quake – jogo que utilizava esta operação frenquentemente e para qual foi desenvolvida. O seu código é:
-
float InvSqrt(float x){
-
float xhalf = 0.5f*x;
-
int i = *(int*)&x;
-
-
i = 0x5F3759DF – (i>>1);
-
x = *(float*)&i;
-
x = x*(1.5f – xhalf*x*x);
-
-
return x;
-
}
Sua verdadeira autoria é desconhecida. Contudo, tudo indica que foi programada por John Carmack, na época programador do Quake 3, onde surgiu.
Para entender esta a genial “sacada” é necessário conhecer o algoritmo matemático usado e como ele foi optimizado graças ao conhecimento do padrão de ponto flutuante pelo autor.
2.O Método de Newton-Rapson
O algoritmo utilizado foi um método de aproximações numéricas já bastante conhecido em muitos projetos computacionais, conhecido como “Aproximação de Newton”, ou “Método de Newton-Rapson”.
Este é um método linear iterativo utilizado para encontrar raízes x de uma função f(x) a partir de uma estimativa inicial baseado na idéia de usar retas tangentes para substituir o gráfico y = f(x) próximo de onde f é zero.
Para tal, cria-se uma função G, chamada “função iteração” xnew = G( xold ) , a partir da função f(x) para se chegar à raiz de f(x). Sendo assim, a convergência deste método será tão rápida quanto for |G’(xold)|.
O método de Newton-Rapson modifica o método iterativo linear simples (Método do Ponto Fixo Modificado) fazendo com que |G’( xold )|->0. Assim, a fórmula iterativa fica:
![]()
Uma propriedade importante para a função InvSqrt é que o Método de Newton-Rapson converge quadraticamente. Uma interpretação gráfica do método pode ser vista na figura abaixo:

2.1.Newton-Rapson na InvSqrt
Queremos o retorno da função y=y(x) tal que:
![]()
contudo, queremos o valor de y, que é a inversa da raíz quadrada. Sendo assim, procuramos a função inversa tal que:
![]()
![]()
![]()
![]()
![]()
como sabemos que o Método de Newton-Rapson para encontrar raízes f(x) = 0, então poderíamos substituir a variável x pela y, que é a que queremos encontrar.
![]()
como
![]()
![]()
![]()
então
![]()
aplicando o Método de Newton, teremos que:
![]()
onde sabemos f(y),
![]()
logo
![]()
finalmente, a expressão para encontrar o valor de y é:
![]()
que pode ser reduzida para:
![]()
ou passando 0.5 para expressão dentro dos parentes,
![]()
lembrando que quanto maior o número de iterações, mais próximo da raiz será o valor do resultado.
3.I3E 754 de 1985
Para entender a maior “mágica” deste código, é necessário saber como os números de ponto flutuante são armazenados na memória e interpretados pelo processador.
Em ciências exatas é comum se ver a notação científica de base 10 para expressar valores muito grandes ou muito pequenos. Esta notação consiste em usar apenas um único dígito à esquerda do valor decimal. Por exemplo, um valor em notação científica na base 10 é:
0,8 x 10^-8
Da mesma forma, podemos expressar números em notação científica em base 2. Por exemplo:
1,0 x 2^7 (em base 2)
Contudo, como não se está trabalhando com a base 10, é conveniente renomear os valores após a vírgula de decimais para ponto binário.
A expressão “ponto flutuante” vêm porque a representação dos números em que o ponto binário não é fixo – por isso flutua. A linguagem em que o InvSqrt foi implementada, C, utiliza esta o nome “float” para estes números de ponto flutuantes.
Desta forma, internamente o computador armazena todos expoentes em base 2, algo como:
1,0 x 10^111 ( binário equivalente à 1,0 x 2^7 )
Observe que a representação de ponto flutuante implica em encontrar uma relação entre o tamanho da fração e o tamanho do expoente, pois o número de bits da palavra do processador é fixo. Assim, quando se aumenta o tamanho da fração, reduz-se o valor do expoente.
Logo, os números em ponto flutuante seguem o formato:
(-1)^s x F x 2^E
onde s é o sinal do número de ponto flutuante de maior relevância e que também é o bit que indicará o sinal do número (s=0 implica em número positivo, s=1, negativo).
F envolve o campo de fração e E são os bits de expoente.
Com base nessa lógica, foi elaborado um padrão para pontos flutuantes chamado “padrão IEEE 754”, que é implementado em quase todos processadores fabricados desde 1985. A adoção deste padrão permitiu uma portabilidade muito maior entre plataformas e melhorou muito a qualidade numérica de operações aritméticas com valores reais.
Para entender o padrão, veja na tabela abaixo como se relacionam os bits na palavra de uma arquitetura de 32 bits:

Note que reservando 8 bits para o expoente temos que 7 bits são utilizados para o valor do expoente e 1 sinal é reservado para o sinal do expoente. Isso equivale em decimal à um range de 2×10^-38 a 2×10^38.
Seguindo a mesma idéia da notação de ponto flutuante de precisão simples (float), é implementada a precisão dupla (tipo “double”). Esta porém, utiliza 2 registradores de 32 bits em arquiteturas com esta palavra de processador, ou 1 registrador de 64 bits para a representação de ponto flutuante com precisão maior.
No caso da precisão dupla em processadores 32 bits, 1 registrador armazena 32 bits para a fração e outro registrador possui a seguinte divisão:

Ou seja, tanto em arquiteturas de 64 quanto de 32 bits são reservados 52 bits para fração (20 mesmo registrador + 32 de um segundo registrador em processadores 32 bits), 11 bits para expoente (1 de sinal + 10 de valor) e 1 bit para o sinal do número.
Logo, a precisão dupla permite um range de valores de 2×10^-308 a 2×10^308.
Outro ponto importante para compreensão do InvSqrt é saber que os projetistas do IEEE 754 construíram esta representação para que se pudesse ser facilmente processada utilizando os mesmos circuitos de comparação de inteiros – provavelmente já implementados nas arquiteturas da época.
O motivo disso está relacionado com o melhor aproveitamento dos registradores (que não precisam ser dedicados à valores de ponto flutuante ou valores inteiros) e para permitir um teste rápido de comparação (instruções bne e beq aproveitadas para qualquer tipo de dado).
Assim, a representação do IEEE 754 pode ser processada com as mesmas comparações de inteiros para acelerar a ordenação dos números de ponto flutuante.
O IEEE 754 é sem dúvida um padrão muito consistente e eficiente e vêm sendo mantido por sua confiabilidade. Além das propriedades descritas aqui, ainda há algumas outras que vale a pena conhecer. Por exemplo, o padrão já prevê um símbolo padrão para operações que geram erros, utilizado por “baixo dos panos” no envio de sinal para o programa tratar uma exceção.
Uma leitura altamente recomendada deste padrão está no livro “Computer Organization and Design”, do Patterson.
4.InvSqrt
Começando pela primeira linha do algoritmo, pode-se adiantar que xhalf equivale ao produto (0.5*x) da expressão obtida no ítem 2.1:
![]()
-
float xhalf = 0.5f*x;// equivalente a expressao acima
este valor é salvo em uma nova variável (xhalf) porque a variável x será reaproveitada para guardar o valor de raiz (chamada de y nas demonstrações). Como visto anteriormente, a variável x (das expressões) só aparece na última expressão (0.5*x).
Na linha seguinte:
-
int i = *(int*)&x;
temos um casting de ponteiros que é a alma deste código. Internamente, o que ocorre é uma mudança na significância dos bits.
Enquanto x é uma variável de ponto flutuante de 32 bits, com a significância para os bits do padrão 754 (1 bit de sinal, 8 para o expoente e 23 para a base), a variável i é uma variável inteira, onde todos os bits possuem a mesma significância.
Todavia, como tanto o tipo de x quanto de i possuem o mesmo tamanho em palavra – 32 bits – é possível armazenar ambas em um mesmo espaço. Isso é o que faz esse código ser possível.
As operações com ponteiros utilizada aqui deve ser lida de trás pra frente. A ordem diz que:
- &x: passe o endereço do espaço de memória onde x ocupa (4 palavras de memoria, ou 32 bits), que suporta tipos de ponto flutuante.
- (int *): converta este espaço de memória para suportar tipos de dados inteiros simples (também 32 bits)
- *: retorne o valor armazenado lá como inteiro.

Na próxima linha,
-
i = 0x5f3759df – (i>>1);
têm-se a estimativa inicial para y[n] = y[0]. Note que a operação (i >> 1), deslocamento de bits à direita, equivale à divisão por 2. O uso desta operação é a justificativa para a conversão da representação de float para int.
Veja que realizando o deslocamento de bits em todo o vetor de bits, também se tira a raíz do expoente. Por exemplo, o deslocamento da representação por inteiro de um valor qualquer:
BIN(01100110101111100010110101010101) = DEC(3447478954)
apenas deslocando os bits à direita 1 vez, ( 01100110101111100010110101010101 >> 1) têm-se que:
BIN(00110011010111110001011010101010) = DEC(1723739477)
onde se vê claramente que 1723739477 = 3447478954/2.
Mas isso não tem nada de excepcional. Contudo, analisando levando em conta a reserva de bits para sinal, base e expoente, da norma IEEE 754 para essa mesma seqüência, vê-se no exemplo:

quando é aplicada o deslocamento de bits à direita, têm-se:

observe que o expoente é exatamente a metade inteira do valor antes do deslocamento dos bits. Da segunda linha das tabelas, no valor decimal, (int) 102.5 == 102 == (int)205/2 .
Então, o motivo do autor do código de converter os bits de uma variável float para uma inteira e deslocar o bits é justamente para dividir o expoente por 2, que obviamente equivale à tirar a raiz.
Em seguida, quando o autor reconverte o valor para ponto flutuante,
-
x = *(float*)&i;
ele simplesmente recoloca os bits na forma interpretada pelo processador como sendo um float do IEEE 754, como explicamos acima.
Sendo assim, após já tirar a raíz do valor de entrada, o método de Newton é aplicado na última linha antes do retorno da função, para convergir ao valor da raiz quadrada:
-
x = x*(1.5f – xhalf*x*x);
-
// xhalf = 0.5x e x = y, nas demonstracoes
conforme demonstrado, o Método de Newton para raiz quadrada inversa:
![]()
5.Conclusões
O código da InvSqrt quando foi desenvolvido, rodava supostamente 4 vezes mais rápido que utilizando (float) 1/sqrt(x), da biblioteca padrão e o erro relativo está por volta de 10^-4. O que tornava o código muito útil.
Com o avanço das otimizações dos compiladores e dos circuitos de multiplicação dos processadores, a função nativa consegue ser mais eficiente. Contudo, conhecer esta função, seu funcionamento e o algoritmo utilizado é engrandecedor para qualquer programador.
A importância desta função foi tamanha que muita discussão foi gerada em torno dela. Inclusive modificações, melhorias e até alguns artigos sobre ela.
Um exemplo disso foi a pesquisa de Chris Lomont (www.math.purdue.edu/~clomont) sobre o melhor valor a ser usado como y[0] no número mágico para que a primeira aproximação tenha convergência mais rápida. Segundo ele, a melhor constante para melhoria da performance seria 0x5f375a86, uma pequena diferença de 167 em relação ao original.
6.Referencias
- Finney, Ross L. CÁLCULO DE GEORGE B. THOMAS JR. , volume 1. Tradução de Paulo Buschcov. Addison Wesley, 2002. Páginas 301-305. “O MÉTODO DE NEWTON”.
- Chris Lomont, www.math.purdue.edu/~clomont, “FAST INVERT SQUARE ROOT”.
- IEEE 754, grouper.ieee.org/groups/754
- Patterson, David A. COMPUTER ORGANIZATION AND DESIGN. 3rd ed. Elsevier, 2005.
Você pode ler as eventuais respostas desta entrada
através do RSS 2.0 feed.
As respostas estão encerradas, mas você pode
trackback
a partir do seu próprio site.