Por que um contador de 64 bits em um loop quebra o desempenho do _mm_popcnt_u64

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

O famoso bug que ainda persegue desenvolvedores C++

\n\n

Em 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.

\n\n

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.

\n\n

O cerne do problema: contador de 32 bits vs. 64 bits

\n\n

Imagine 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:

\n\n
#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\n

Ao 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\n

Por que isso acontece? A raiz do mal — falsa dependência

\n\n

A 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.

\n\n

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.

\n\n

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.

\n\n

Análise detalhada em assembly

\n\n

Para confirmar, vamos ver como cada versão é compilada (GCC 13, flags -O3 -march=native):

\n\n

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\n

Contador de 64 bits (lento):

\n
.L7:\n    popcnt rax, QWORD PTR [rdx]   ; popcnt em rax — o mesmo que o                

Blogs

Book Recommendations