Medir a performance, SEMPRE! (floats versus doubles)

Eu sou uma besta quadrada… admito… Em um post anterior afirmei (pelo menos acho que fiz isso) que usar floats ao invés de doubles é mais performático, em se tratando de operações em ponto flutuante. Sempre tive essa crença por dois motivos:

  • float é um tipo menor do que double. O primeiro tem exatamente 32 bits de tamanho. O segundo, 64. É lógico pensar que as inicializações e atribuições sejam feitas de forma mais veloz com floats do que com doubles, já que apenas um registrador genérico estará envolvido;
  • Pelo fato de float ser menor e ter menos precisão do que doubles, assumi que as operações (adição, subtração, multiplicação, divisão, funções trigonométricas, etc) fossem também mais rápidas
A minha crença, no primeiro caso, é justificada: Atribuições e cópias são, de fato, mais rápidas na arquitetura x86. Mas a segunda crença é falsa. Vejam isso:
#include <stdint.h>
#include "rdtsc.h"

int main(int argc, char **argv)
{
  float fa, fb, fc;
  double da, db, dc;
  long double lda, ldb, ldc;
  uint64_t t;
  int cnt;

  fb = fc = 0.1f;
  BEGIN_RDTSC(t);
  for (cnt = 0; cnt < 1000; cnt++)
    fa = fb + fc;
  END_RDTSC(t);
  printf_rdtsc_results("1000 float adds", t);  

  db = dc = 0.1;
  BEGIN_RDTSC(t);
  for (cnt = 0; cnt < 1000; cnt++)
    da = db + dc;
  END_RDTSC(t);
  printf_rdtsc_results("1000 double adds", t);

  ldb = ldc = 0.1;
  BEGIN_RDTSC(t);
  for (cnt = 0; cnt < 1000; cnt++)
    lda = ldb + ldc;
  END_RDTSC(t);
  printf_rdtsc_results("1000 long double adds", t);

  return 0;
}

Adicionei a função print_rdtsc_results() em rdtsc.c:

void printf_rdtsc_results(const char *sz, uint64_t t)
{
  printf("%s: %llu cycles\n", sz, t);
}

Ao compilar o código e executá-lo, eis o que obtive:

$ gcc -mtune=native -O0 -fomit-frame-pointer -o test test.c rdtsc.c
$ ./test
1000 float adds: 9849 cycles
1000 double adds: 5901 cycles
1000 long double adds: 8295 cycles

Ou seja, operações de adição com floats são cerca de 67% mais lentas do que as mesmas operações com doubles. São tão performáticas quanto o uso de long doubles (que tem 80 bits – 10 bytes de tamanho – e não podem ser copiadas de uma só vez pelo processador!).

A explicação para essas discrepâncias é a seguinte: Todas as operações em ponto flutuante são feitas como long doubles na arquitetura Intel. Quando usamos float, o processador tem o trabalho de convertê-lo para o tipo long double. Essa conversão toma tempo desnecessário… O mesmo acontece com o tipo double, mas a conversão é mais rápida. Quanto ao tipo long double, como expliquei, o problema não está na conversão, mas no ciclo adicional necessário para copiar os 10 bytes para a pilha do processador. Os 64 bits inferiores são copiados num ciclo e os 16 superiores no outro.

Ué?! Não estamos falando de arquitetura de 32 bits? Vale lembrar que o processador, desde o primeiro Pentium, realiza leituras e escritas usando um barramento de 64 bits de tamanho! Você até poderia pensar em forçar a barra e alinhar os tipos float de 8 em 8 bytes (64 bits boundary) assim:

typedef float __attribute__((aligned(8))) Float;

Basta trocar os tipos float por Float no código inicial. Mas, você observará que isso não mudará nada. A performance das adições em float permanecerá a mesma, corroborando a explicação sobre a conversão de tipos, pelo processador.

Outra fonte de confusão, pelo menos para mim, é a documentação do OpenGL que afirma:

We require simply that numbers’ floating-point parts contain enough bits and that their exponent fields are large enough so that individual results of floatingpoint operations are accurate to about 1 part in 105. The maximum representable magnitude of a floating-point number used to represent positional, normal, or texture coordinates must be at least 232. [p.7-8, OpenGL 4.1 Specification (Core Profile)]

Eu assumi que OpenGL fosse otimizado para atender esses requisitos e, por isso, o tipo float seria mais performático. Digo que sou uma besta quadrada porque não li a primeira frase do último parágrafo da página 7 do manual:

We do not specify how floating-point numbers are to be represented, or the details of how operations on them are performed. (o negrito é meu)

Estou batendo a cabeça na parede até agora, acreditem…

Num próximo artigo falo da comparação de performance entre SSE (que não permite o uso de doubles) e SSE2. Tenho a crença de que a discrepância mostrada ai em cima não vale para SSE, veremos…

Anúncios

Um comentário sobre “Medir a performance, SEMPRE! (floats versus doubles)

  1. Coolface… Bwhawhawha!

    Coitada da parede, véi…

    Eu sei como vc tem a cabeça dura e gosta de testar se a dos outros tb é…

    Lembro bem daquela que vc já me contou sobre a prática do seu “judô” em tempos antigos!

    Pega leve contigo, todos erramos. Até o William Bonner erra!

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