PHP para Iniciantes: Tipos de Dados Primitivos - Pontos Flutuantes

Um dos assuntos mais punks que existe, na minha opinião, é ponto flutuante. É complexo, ainda mais pra quem está acostumado a transitar entre diversas linguagens. Eu, como "engenheiro de software", carrego no coração a ideia de que não podemos nos prender a nenhuma linguagem, sem exceção. A gente tem de trabalhar com a ferramenta certa nas horas certas. Mas aritmética é algo que existe em todas - talvez uma tenha melhor performance, mas definitivamente todas podem calcular.

E, mais uma vez, esse tipo veio da matemática. Especificamente falando sobre aritmética de ponto flutuante. Se você nunca ouviu falar, eu vou até mencionar alguns exemplos mas não vou me aprofundar sobre isso agora, tudo bem? Apenas compreenda que floats foram feitos para fazer cálculos com números fora do domínio de números inteiros.

A forma de escrever um ponto flutuante em PHP é bem simples: você pode informar um número com sua fração (número.fração), só a parte fracionária (.fração) ou até um número exponencial (númeroEexpoente, onde número pode já ser as outras notações de float mencionadas anteriormente). Exemplos:

  • 1.0 (que é diferente do inteiro 1);
  • .14 = 0.14;
  • 1E4 = 10000.0;
  • 1.1E4 = 11000.0;

Bem fácil, não é mesmo? Mas quando chega a hora de lidar com pontos flutuantes usando frações absurdamente pequenas e com a exigência de muita precisão, as pernas estremecem, rs. Se você não teme lidar com isso, meus parabéns. Não sei se é porque tem completo domínio, se realmente não sabe a chance de gerar erros ou se já conhece algumas funções de cálculo de precisão arbitrária ( ͡° ͜ʖ ͡°), mas assumir os riscos de realizar cálculo preciso com pontos flutuantes só com as operações nativas é muita coragem.

Riscos e Erros

Quando você faz somas, subtrações, divisões ou qualquer outra operação matemática com esses números, algo de ruim poderá acontecer. Na prática, o que costumamos fazer para manter essa legibilidade do código é converter o que quer que esteja escrito em ponto flutuante para inteiros (tipo de dado mencionado no artigo anterior).

Como assim, Kiko?

É muito comum encontrar projetos com cálculos monetários que convertem os valores para centavos ao invés de trabalhar com reais. O motivo dessa conversão toda é justamente pra fugir dos riscos de fazer cálculos com pontos flutuantes. Por exemplo, ao invés de fazer a conta 2.45 - 1.01 - 1.44 (R$2,45 - R$1,01 - R$1,44), você pode fazer 245 - 101 - 144 (245c - 101c - 144c, onde c representa centavos). Só essa conta que mencionei já mostra resultados diferentes no PHP. A conta com centavos resulta em 0, como esperado. Já a conta em reais, resulta 2.2204460492503E-16.

QUÊ?

Pois é! Bugante, né? De toda forma, esse tipo de erro pode acontecer em qualquer linguagem, não é exclusivo do PHP. E tudo isso só acontece dessa forma pelo que mencionei no artigo anterior: limitações computacionais.

Mas Kiko, esses erros acontecem com números tão pequenos... Como isso pode ser limitação computacional??

Hehehe, lá nos inteiros, a limitação computacional influencia diretamente no overflow, correto? Tem um inteiro máximo e um inteiro mínimo. Mas você já se perguntou sobre o que acontece quando você ultrapassa esses limites computacionais no PHP?

Re: o interpretador converte o número para float!

E isso porque pontos flutuantes possuem uma estratégia de armazenamento completamente diferente dos inteiros. Enquanto os inteiros usam todo o comprimento binário para representar o seu dado, o ponto flutuante precisa armazenar tanto a parte inteira quanto a parte fracionária, mesmo se não tiver valor algum na fração, e ainda assim tudo em base binária - que no armazenamento não quer dizer nada de demais, mas nas operações o bagulho fica louco.

Caraca... Não tô entendendo nada...

Tudo bem, vamos quebrar as nossas cabeças de uma vez. Primeiro, preciso que tenha em mente que o PHP usa o padrão IEEE-754 em sua aritmética de ponto flutuante. Sabendo disso, podemos pesquisar calculadoras e conversores para fazermos alguns testes. Um site bacana para validarmos os valores que irei informar aqui (ou se quiser testar outros números) é o IEEE-754 Floating-Point Converter. Então, vamos começar:

  • o binário do inteiro 1 é 1(ooooohhhhhhhhhh);
  • o bináro do ponto flutuante 1.0é 0011 1111 1000 0000 0000 0000 0000 0000 ( O_O! );
  • e se aumentarmos 0.01, temos que 1.01 é 0011 1111 1000 0001 0100 0111 1010 1110 ( O__O )

Como assim?!?! Por que tem tantos 1s no ponto flutuante??

Lembra que eu disse que 1 != 1.0? Pois é! O custo para fazer cálculo numérico nem se compara... Por exemplo, naquela conta que mencionei acima, os binários seriam:

  • Int (245 - 101 - 144): 1111 0101 - 0110 0101 - 1001 0000 = 0 (usei a função decbin do PHP para confirmar os binários);
  • Float (2.45 - 1.01 - 1.44): 0100 0000 0001 1100 1100 1100 1100 1101 - 0011 1111 1000 0001 0100 0111 1010 1110 - 0011 1111 1011 1000 0101 0001 1110 1100 = 0010 0101 1000 0000 0000 0000 0000 0000.

Percebe que a conta em float, por menor que pareça o número, sempre vai custar mais recurso computacional?? E sobre não resultar 0, acontece porque todos esses binários que eu mencionei agora há pouco não representam os valores precisos que nós queríamos contar. Foram valores arredondados baseado nos expoentes de 2!

E aí, ao invés de 2.45, nós usamos 2.4500000476837158203125. Ao invés de 1.01, nós usamos 1.0099999904632568359375. E ao invés de 1.44, 1.440000057220458984375.

Nenhum dos números da conta foi um valor fechado. Suas minúsculas variações resultaram, no fim, em uma conta que não bate com o que desejamos, porque na essência os dados já eram diferentes. É preciso tomar muito cuidado com isso quando está lidando com pontos flutuantes, INDEPENDENTE DA LINGUAGEM!

Então, por favor, lembre desse artigo com carinho quando quiser fazer operações monetárias. Esse assunto é tão complexo que tem vários papers falando sobre isso. O mais legal que vi é um super resumo chamado "What Every Computer Scientist Should Know About Floating-Point Arithmetic". Caso esteja em dia no inglês e seja da área, vale a pena dar uma lida.

Mas Kiko, se é tão arriscado assim, por que existe esse tipo então??

Calma, tem lá seus benefícios.

Os Acertos

A verdade é que ponto flutuante tem melhor performance quando estamos falando de números muito grandes ou muito pequenos (negativamente falando). Enquanto inteiros tem uma limitação computacional bem definida, os floats podem ir além!

É por isso que quando passamos do limite dos inteiros, o PHP automaticamente converte para float. Faz o teste:

<?php
echo PHP_INT_MAX + 1;

Esse código irá pegar o maior valor inteiro possível no seu ambiente e somar 1. Em algumas linguagens isso daria um erro de overflow de memória. Geralmente essas são linguagens de tipagem forçada, como mencionei no artigo de introdução desse tema. Como a linguagem não aceita a troca, ela morre ali mesmo.

Dito isso, se até a linguagem aceita que, se você quer lidar com números exorbitantes, o melhor é usar float, por que você negaria? Se no propósito das suas contas há o risco de lidar com frações muito pequenas, o ideal é trabalhar com inteiros. Mas se os números vão, em algum momento, ficar grandes o suficiente que possa ultrapassar o limite do sistema, então trabalha com float!

A precisão para números grandes é bem confiável - desde que as variações não sejam nas casas decimais. E com isso você terá um bom controle dos seus dados.

Tá, Kiko, cadê o exemplo?

Sinceramente, não tenho uma conta real para que possam associar aqui. Eu nunca programei jogos em PHP, mas se o fizesse, alguns cálculos seriam com float. Principalmente no que diz respeito a detector de movimento/colisão, onde há uma grande taxa de FPS e os cálculos precisam ocorrer a cada clock da máquina. Isso ocorre MUITO quando você precisa lidar com manipulação gráfica, também. E isso vai além. Portanto, ponto flutuante tem sim um papel importante na vida dos devs.

Kiko, lá no começo tu mencionou umas funções...

Ah, sim! É verdade. No caso de precisar lidar com aritmética de ponto flutuante, NÃO USE AS OPERAÇÕES NATIVAS DO PHP. Há funções mais seguras quando envolve isso e que garantem um resultado mais próximo da realidade, pois auxiliam nas operações de arredondamento dos números.

Na documentação do PHP, eles chamam essas funções de BC Math e você pode encontrar todo o guia disso aqui: https://www.php.net/manual/pt_BR/book.bc.php.

Por exemplo, lembra daquela conta 2.45 - 1.01 - 1.44? A forma correta de se fazer essa conta é usando a função bcsub:

<?php
echo 2.45 - 1.01 - 1.44; // errado
echo bcsub(bcsub('2.45', '1.01'), '1.44'); // certo

E por mais chato que isso seja, se você realmente precisa lidar com ponto flutuante, essa é a forma mais segura.

Captou? Curtiu? Compartilha com os amigos e diz aí qual parte do artigo te chamou mais atenção! Dessa vez eu levei mais tempo para escrever, apaguei um monte de coisa porque tinha me aprofundado demais e fui reescrevendo... Não tem como não se aprofundar nesse tipo de dado, e sim, tem muuuito mais detalhes que não comentei porque ainda quero que estude PHP, hahahaha. Mas se ficou alguma dúvida, não hesite em questionar aí nos comentários, beleza?!

Inté!