Um cuidado necessário ao lidar com ponto-flutuante…

Eis duas rotinas que, aparentemente, fazem a mesmíssima coisa:

/* Ambas as rotinas fazem:

    f(x)*dx = (((y2 + y1) / 2.0) + ((y2 - 2*y1 + y0) / 48.0)) * dx;
*/

float integr_step(float y2, float y1, float y0, float dx)
{
  return ((y2 + y1)*0.5f + (y2 - 2.0f*y1 + y0) / 48.0f)*dx;
}

float integr_step2(float y2, float y1, float y0, float dx)
{
  return ((y2 + y1)*0.5 + (y2 - 2.0*y1 + y0) / 48.0)*dx;
}

Elas são iguais, certo?

Não até você ver o código em assembly gerado:

$ gcc -O3 -mtune=intel -masm=intel -msse2 -S test.c
$ cat test.s
...
integr_step:
  movaps xmm4, xmm0
  addss xmm4, xmm1
  addss xmm1, xmm1
  mulss xmm4, DWORD PTR .LC0[rip] ; .LC0 = (float)0.5
  subss xmm0, xmm1
  addss xmm2, xmm0
  divss xmm2, DWORD PTR .LC1[rip] ; .LC1 = (float)48.0
  addss xmm2, xmm4
  mulss xmm2, xmm3
  movaps xmm0, xmm2
  ret
...
integr_step2:
  xorpd xmm4, xmm4     
  movaps xmm5, xmm0
  cvtss2sd xmm4, xmm1
  addss xmm5, xmm1
  cvtss2sd xmm0, xmm0
  cvtss2sd xmm2, xmm2
  cvtss2sd xmm5, xmm5
  addsd xmm4, xmm4
  cvtss2sd xmm3, xmm3
  subsd xmm0, xmm4
  mulsd xmm5, QWORD PTR .LC3[rip]  ; .LC3 = (double)0.5
  addsd xmm2, xmm0
  xorps xmm0, xmm0
  divsd xmm2, QWORD PTR .LC4[rip]  ; .LC4 = (double)48.0
  addsd xmm2, xmm5
  mulsd xmm2, xmm3
  cvtsd2ss xmm0, xmm2
  ret
  ...

A diferença de tamanho é justificada graças as conversões de float para double que a segunda rotina é obrigada a fazer. Note que as constantes são 0.5 e 48.0 (não 0.5f e 48.0f). O compilador sempre usará a maior precisão, se houver uma mistura e, no fim das contas, converterá para a precisão menor.

Isso pode resolver alguns problemas de precisão com float (fazendo as contas em double), mas cria código bem maior e mais lento. A instrução CVTSS2SD, nas arquiteturas mais novas como a Haswell, gasta 1 ciclo (gastava de 8 a 9 antes). Note que antes da conversão é necessário zerar toda a parte inferior de um registrador XMM via XORPD, que gasta 2 ciclos (gastava 4). Antes de voltar de double para float XORPS/CVTSD2SS são usados, o que gasta 3 ciclos (2 de XORPS e 1 de CVTSD2SS), costumava gastar uns 10… No todo a mesma rotina gasta 10 ciclos de máquina extras, sem contar com as operações em dupla precisão…

A dica aqui é simples: Se vai usar floats, mantenha suas variáveis e constantes em float – a não ser que você realmente precise aumentar a exatidão do cálculo. Esquecer de colocar o ‘f’ (ou ‘F’) depois de uma constante em ponto flutuante faz com que ela seja double e o compilador converterá tudo o que tiver que converter para realizar as operações em double!

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s