Três critérios para escolha da precisão

De novo venho falar para vocês sobre ponto flutuante. Já vimos que, nas arquiteturas modernas, o tipo double tende a criar código um pouco mais veloz que o uso do tipo float, mas isso depende da quantidade de valores que você está trabalhando.

O primeiro critério é justamente esse e relaciona-se com o cache L1d: Quanto maiores forem arrays ou conjuntos de dados com que você tem que trabalhar, mais cache misses você terá e mais lenta ficará sua aplicação. Neste aspecto o uso do tipo float pode ser vantajoso, mesmo que tenhamos menos precisão. Note que um array com 1000 floats ocupará 4000 bytes, enquanto a mesma quantidade de doubles ocupará o dobro do espaço, colocando mais pressão nos caches.

Os outros dois critérios tem a ver com a precisão. Considere o caso de aplicações em computação gráfica, usando OpenGL. Num ambiente desses é comum lidamos apenas com valores entre zero e 1.0. O tipo float, nessa condição, nos dá a exatidão máxima de \pm 2^{-23} ou \pm 1.19209290\cdot10^{-7}. Isso nos dá um erro de, aproximadamente, 0.12 ppm (partes por milhão). Já um double apresenta exatidão máxima de \pm 2^{-52} ou \pm 2.220446049250313081\cdot10^{-16} o que significa um erro de, aproximadamente, 0.00000000023 ppm. Ambos os erros são muito pequenos para esse tipo de aplicação (computação gráfica). Vale lembrar que 1 ppm é a mesma coisa que 0.0001 %. Embora o menor valor representável dada uma faixa bem definida seja um critério, outro é mais importante: O erro de truncamento.

Truncamentos ocorrem quando o valor representado não pode sê-lo pela quantidade de bits disponível. Isso é particularmente prejudicial quando lidamos com valores grandes. O tipo float armazena 24 bits e os desloca de acordo com a escala fornecida no expoente. Isso significa que o maior valor binário exato que pode ser expresso tem exatamente 24 bits setados (0b1.11111111111111111111111 >> 23) ou 16777215.0. A faixa de exatidão superior do tipo double é bem maior: 9007199254740991.0. Valores além desses terão os bits inferiores truncados (zerados). Um valor maior que 1.6777215\cdot10^7, por exemplo 2\cdot10^7, ao ser incrementado, continuará sendo 2\cdot10^7. O bit final não pode ser armazenado num valor desses. Neste caso o uso de um tipo com precisão maior, como double, vale à pena, mesmo com o custo do recurso de memória.

Assim os três critérios são esses:

Critério float double
Tamanho (em bytes) 4 8
Exatidão (aproximada) \pm 0.12\,ppm \pm 2.3\cdot10^{-10}\,ppm
Máximo inteiro exato 16777215 9007199254740991

O tipo long double:

Eu insisto em dizer que o tipo long double deve ser evitado em qualquer código. Em primeiro lugar ele coloca ainda mais pressão nos caches do que o tipo double porque tem 10 bytes de tamanho. Em segundo lugar, pelo fato de ter 10 bytes, ele não pode ser carregado em nenhum registrador do processador… A cópia de uma variável para outra é uma simples questão de movimento de um registrador para outro (ou para a memória). Um float cabe direitinho em registradores como EAX, por exemplo. E double cabe em RAX. Mas não há equivalência para um long double. Isso faz com que o compilador não tenha alternativa a não ser usar o co-processador matemático e, para isso, precisará empilhar e desempilhar valores, usando o instruction set do 80×87 ao invés de SSE ou AVX.

Em terceiro lugar, a exatidão deste tipo, mesmo sendo muito maior que a do tipo double, apresenta um ganho insignificante, mesmo em aplicações científicas. E, por último, o inteiro máximo positivo que pode ser armazenado sem erros de truncamento é 18446744073709551615. Bem maior que o possível em double, no entanto, de novo, mesmo para aplicações científicas isso é irrelevante.

Para efeitos de comparação, eis a mesma tabela, mas apenas com o tipo long double:

Critério long double
Tamanho (em bytes) 10
Exatidão (aproximada) \pm 3.4\cdot10^{-4926}\,ppm
Máximo inteiro exato 18446744073709551615
Anúncios