O famoso bug que ainda persegue desenvolvedores C++
\n\nEm 2013, uma pergunta no Stack Overflow reuniu centenas de milhares de visualizações e criou um verdadeiro culto entre otimizadores de C++. A pergunta era mais ou menos assim: "Troquei int por uint64_t no contador do loop — e o desempenho caiu 10 vezes. Por quê?". A resposta não estava na magia, mas na microarquitetura dos processadores Intel. Em 2025, esse problema não desapareceu: chips modernos ainda podem "tropecar" com uma substituição aparentemente inofensiva de tipo de dado.
Hoje, vamos analisar esse paradoxo usando a instrução _mm_popcnt_u64 (contagem de bits definidos em uma palavra de 64 bits), mostrar código real que sofre com essa anomalia e dar recomendações claras sobre como escrever código C++ rápido e previsível em 2025.
O cerne do problema: contador de 32 bits vs. 64 bits
\n\nImagine um loop simples que soma a quantidade de bits definidos (popcount) em um array de inteiros de 64 bits. Usamos o intrínseco _mm_popcnt_u64 do conjunto SSE4.2. Aqui estão duas versões do código — a primeira com um contador de 32 bits, a segunda com um de 64 bits:
#include <cstdint>\n#include <vector>\n#include <nmmintrin.h> // para _mm_popcnt_u64\n\n// Versão 1: contador de 32 bits (rápido)\nuint64_t popcnt_32bit(const std::vector<uint64_t>& data) {\n uint64_t result = 0;\n for (int i = 0; i < data.size(); ++i) { // int — 32 bits\n result += _mm_popcnt_u64(data[i]);\n }\n return result;\n}\n\n// Versão 2: contador de 64 bits (lento)\nuint64_t popcnt_64bit(const std::vector<uint64_t>& data) {\n uint64_t result = 0;\n for (uint64_t i = 0; i < data.size(); ++i) { // uint64_t — 64 bits\n result += _mm_popcnt_u64(data[i]);\n }\n return result;\n}\n\n\nAo medir a velocidade em um processador Intel (por exemplo, Core i7-13700K ou mesmo um Haswell mais antigo), a diferença será chocante: a segunda versão pode ser de 3 a 10 vezes mais lenta. Já no AMD Ryzen, essa anomalia não existe — ambas as versões funcionam igualmente rápido.
\n\nPor que isso acontece? A raiz do mal — falsa dependência
\n\nA instrução POPCNT em processadores Intel (até os mais modernos) tem uma falsa dependência (false dependency) no registrador de destino. Formalmente, POPCNT sobrescreve completamente o registrador de saída, mas microarquiteturalmente o processador se comporta como se estivesse lendo o resultado antigo antes de escrever. Isso é um bug (ou característica) da implementação da Intel.
Agora, observe o loop com o contador de 64 bits i. O compilador é forçado a usar o mesmo registrador (por exemplo, rax) para armazenar tanto o contador quanto os resultados intermediários do POPCNT. Cada iteração cria uma cadeia de dependências: o próximo POPCNT não pode começar até que o anterior termine, porque o processador "acha" que precisa do resultado antigo. Isso destrói o pipeline.
Com o contador de 32 bits, o compilador usa registradores diferentes (por exemplo, ecx para o contador e rax para acumular o resultado), e a falsa dependência não ocorre — os POPCNT são executados em paralelo.
Análise detalhada em assembly
\n\nPara confirmar, vamos ver como cada versão é compilada (GCC 13, flags -O3 -march=native):
Contador de 32 bits (rápido):
\n.L3:\n popcnt rcx, QWORD PTR [rdx] ; popcnt em rcx (registrador independente)\n add rax, rcx ; soma com o acumulador\n add rdx, 8 ; desloca o ponteiro\n cmp rdi, rdx ; comparação\n jne .L3\n\n\nContador de 64 bits (lento):
\n.L7:\n popcnt rax, QWORD PTR [rdx] ; popcnt em rax — o mesmo que o