Por qué un contador de 64 bits en un bucle rompe el rendimiento de _mm_popcnt_u64

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

El famoso error que aún persigue a los desarrolladores de C++

\n\n

En 2013, apareció en Stack Overflow una pregunta que acumuló cientos de miles de visitas y creó un verdadero culto entre los optimizadores de C++. Sonaba más o menos así: «Cambié int por uint64_t en el contador del bucle y el rendimiento cayó 10 veces. ¿Por qué?». La respuesta no estaba en la magia, sino en la microarquitectura de los procesadores Intel. En 2025, este problema no ha desaparecido: los chips modernos aún pueden «tropezar» con un cambio de tipo de datos aparentemente inofensivo.

\n\n

Hoy analizaremos esta paradoja usando la instrucción _mm_popcnt_u64 (conteo de bits en una palabra de 64 bits), mostraremos código real que sufre esta anomalía y daremos recomendaciones claras sobre cómo escribir código C++ rápido y predecible en 2025.

\n\n

El núcleo del problema: contador de 32 bits vs 64 bits

\n\n

Imagina un bucle simple que suma la cantidad de bits en 1 (popcount) en un arreglo de enteros de 64 bits. Usamos la intrínseca _mm_popcnt_u64 del conjunto SSE4.2. Aquí hay dos variantes de código: la primera con un contador de 32 bits, la segunda con uno de 64 bits:

\n\n
#include <cstdint>\n#include <vector>\n#include <nmmintrin.h> // para _mm_popcnt_u64\n\n// Variante 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// Variante 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

Si mides la velocidad en un procesador Intel (por ejemplo, Core i7-13700K o incluso un Haswell más antiguo), la diferencia será impactante: la segunda variante puede ser de 3 a 10 veces más lenta. En AMD Ryzen, en cambio, no existe tal anomalía: ambas variantes funcionan igual de rápido.

\n\n

¿Por qué ocurre esto? La raíz del mal: la falsa dependencia

\n\n

La instrucción POPCNT en procesadores Intel (hasta los más modernos) tiene una falsa dependencia (false dependency) en el registro de destino. Formalmente, POPCNT sobrescribe completamente el registro de salida, pero a nivel microarquitectónico el procesador se comporta como si leyera el resultado anterior antes de escribir. Esto es un error (o una característica) de la implementación en Intel.

\n\n

Ahora observa el bucle con el contador de 64 bits i. El compilador se ve obligado a usar el mismo registro (por ejemplo, rax) para almacenar tanto el contador como los resultados intermedios de POPCNT. Cada iteración crea una cadena de dependencias: la siguiente POPCNT no puede comenzar hasta que la anterior termine, porque el procesador «piensa» que necesita el resultado anterior. Esto destruye el pipeline.

\n\n

Con el contador de 32 bits, el compilador usa registros diferentes (por ejemplo, ecx para el contador y rax para acumular el resultado), y la falsa dependencia no aparece: las POPCNT se ejecutan en paralelo.

\n\n

Análisis detallado en ensamblador

\n\n

Para estar seguros, veamos en qué se compila cada variante (GCC 13, flags -O3 -march=native):

\n\n

Contador de 32 bits (rápido):

\n
.L3:\n    popcnt rcx, QWORD PTR [rdx]   ; popcnt en rcx (registro independiente)\n    add    rax, rcx                ; sumamos al acumulador\n    add    rdx, 8                  ; desplazamiento del puntero\n    cmp    rdi, rdx                ; comparación\n    jne    .L3\n
\n\n

Contador de 64 bits (lento):

\n
.L7:\n    popcnt rax, QWORD PTR [rdx]   ; popcnt en rax — el mismo que el acumulador                

Blogs

Book Recommendations