Introdução: o bug que custou 10x de desempenho
\n\nImagine a situação: você está escrevendo código de alto desempenho em C++. Tudo voa, os testes passam, você se sente um gênio da otimização. E então você muda uma única constante — 0.1f para 0. E o desempenho cai 10 vezes. Sem brincadeira. Sem exagero. Foi exatamente essa pergunta que recebeu milhares de visualizações no Stack Overflow e se tornou um clássico para quem quer entender como funcionam os números de ponto flutuante no nível do processador.
Neste artigo, vamos analisar por que uma substituição aparentemente inofensiva leva a um desastre, quais mecanismos internos do processador são responsáveis por isso e, o mais importante — como evitar isso. O artigo será útil tanto para desenvolvedores C++ iniciantes quanto para engenheiros experientes que desejam entender mais profundamente a otimização de código C++.
\n\nO cerne do problema: números desnormalizados e seu impacto no desempenho do C++
\n\nPara começar, vamos reproduzir o problema. Considere um loop simples que realiza cálculos com ponto flutuante:
\n\n#include <iostream>\n#include <chrono>\n\nvoid funcao_lenta(float factor) {\n float sum = 0.0f;\n for (int i = 0; i < 100000000; ++i) {\n sum += factor * 0.1f; // Rápido, se factor = 0.1f\n // sum += factor * 0; // Lento, se factor = 0\n }\n std::cout << sum << std::endl;\n}\n\nint main() {\n auto start = std::chrono::high_resolution_clock::now();\n funcao_lenta(0.1f); // Tente substituir por 0\n auto end = std::chrono::high_resolution_clock::now();\n std::cout << "Time: " \n << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() \n << " ms" << std::endl;\n return 0;\n}\n\nSe você executar este código com factor = 0.1f, o tempo de execução será, digamos, 100 ms. Substitua por factor = 0 — e você obterá 1000 ms ou mais. Uma diferença de 10 vezes. Por quê?
A resposta está nos números desnormalizados (denormalizados). Esta é uma das armadilhas mais traiçoeiras na otimização C++, que poucos conhecem até se depararem pessoalmente com ela.
\n\nO que são números desnormalizados e por que eles tornam o código lento?
\n\nO padrão IEEE 754 para números de ponto flutuante define não apenas números normais (onde a mantissa começa com 1), mas também números desnormalizados (subnormais). Eles são usados para representar valores muito pequenos, próximos de zero. Quando o resultado de um cálculo se torna menor que o menor número normal, o processador entra no modo de processamento de desnormalizados.
\n\nO problema é que o suporte de hardware para desnormalizados em processadores x86 é extremamente lento. Enquanto operações normais com float são executadas em 1-3 ciclos, operações com desnormalizados podem levar de 100 a 200 ciclos ou mais. Esta é a causa da queda de desempenho.
\n\nVoltando ao nosso exemplo. Quando factor = 0.1f, todos os resultados intermediários permanecem na faixa normal. Mas quando factor = 0, a multiplicação por zero resulta em zero. Em seguida, adicionamos 0.1f a zero — obtemos 0.1f. Mas na próxima iteração, multiplicamos novamente por zero e obtemos zero novamente. E assim por diante, milhões de vezes. Como resultado, o processador gera constantemente valores desnormalizados, levando a uma desaceleração catastrófica.
Experimento: como ver a diferença com seus próprios olhos
\n\nVamos escrever um exemplo mais claro que mostrará como os desnormalizados afetam o desempenho do código C++:
\n\n#include <iostream>\n#include <chrono>\n#include <cmath>\n\nvoid testar_desempenho(float initial_value) {\n float x = initial_value;\n auto start = std::chrono::high_resolution_clock::now();\n \n for (int i = 0; i < 100000000; ++i) {\n x = x * 0.5f + 0.1f; // Fórmula recorrente típica\n }\n \n