Pourquoi un compteur 64 bits dans une boucle brise les performances de _mm_popcnt_u64

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Le célèbre bug qui hante encore les développeurs C++

\n\n

En 2013, une question est apparue sur Stack Overflow, rassemblant des centaines de milliers de vues et créant un véritable culte parmi les optimiseurs C++. Elle disait à peu près ceci : « J'ai remplacé int par uint64_t dans le compteur de ma boucle — et les performances ont chuté de 10 fois. Pourquoi ? ». La réponse ne tenait pas de la magie, mais de la microarchitecture des processeurs Intel. En 2025, ce problème n'a pas disparu : les puces modernes peuvent encore « trébucher » sur ce qui semble être un changement de type de données anodin.

\n\n

Aujourd'hui, nous allons analyser ce paradoxe avec l'instruction _mm_popcnt_u64 (comptage de bits à 1 dans un mot 64 bits), montrer du code réel qui souffre de cette anomalie, et donner des recommandations claires pour écrire du code C++ rapide et prévisible en 2025.

\n\n

Le cœur du problème : compteur 32 bits vs 64 bits

\n\n

Imaginez une boucle simple qui additionne le nombre de bits à 1 (popcount) dans un tableau d'entiers 64 bits. Nous utilisons l'intrinsèque _mm_popcnt_u64 de l'ensemble SSE4.2. Voici deux versions du code — la première avec un compteur 32 bits, la seconde avec un compteur 64 bits :

\n\n
#include <cstdint>\n#include <vector>\n#include <nmmintrin.h> // pour _mm_popcnt_u64\n\n// Version 1 : compteur 32 bits (rapide)\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// Version 2 : compteur 64 bits (lent)\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 vous mesurez la vitesse sur un processeur Intel (par exemple, Core i7-13700K ou même un Haswell plus ancien), la différence sera choquante : la deuxième version peut être 3 à 10 fois plus lente. Sur AMD Ryzen, en revanche, cette anomalie n'existe pas — les deux versions fonctionnent aussi rapidement l'une que l'autre.

\n\n

Pourquoi cela se produit-il ? La racine du mal : la fausse dépendance

\n\n

L'instruction POPCNT sur les processeurs Intel (jusqu'aux plus récents) a une fausse dépendance (false dependency) sur le registre de destination. Formellement, POPCNT réécrit entièrement le registre de sortie, mais au niveau microarchitectural, le processeur se comporte comme s'il lisait l'ancien résultat avant d'écrire. C'est un bug (ou une particularité) de l'implémentation chez Intel.

\n\n

Regardons maintenant la boucle avec le compteur 64 bits i. Le compilateur est obligé d'utiliser le même registre (par exemple, rax) pour stocker à la fois le compteur et les résultats intermédiaires de POPCNT. Chaque itération crée une chaîne de dépendances : la POPCNT suivante ne peut pas commencer tant que la précédente n'est pas terminée, car le processeur « pense » qu'elle a besoin de l'ancien résultat. Cela tue le pipeline.

\n\n

Avec un compteur 32 bits, le compilateur utilise des registres différents (par exemple, ecx pour le compteur et rax pour l'accumulation du résultat), et la fausse dépendance ne se produit pas — les POPCNT s'exécutent en parallèle.

\n\n

Analyse détaillée en assembleur

\n\n

Pour nous en convaincre, regardons en quoi chaque version est compilée (GCC 13, flags -O3 -march=native) :

\n\n

Compteur 32 bits (rapide) :

\n
.L3 :\n    popcnt rcx, QWORD PTR [rdx]   ; popcnt dans rcx (registre indépendant)\n    add    rax, rcx                ; addition avec l'accumulateur\n    add    rdx, 8                  ; décalage du pointeur\n    cmp    rdi, rdx                ; comparaison\n    jne    .L3\n
\n\n

Compteur 64 bits (lent) :

\n
.L7 :\n    popcnt rax, QWORD PTR [rdx]   ; popcnt dans rax — le même que sur                

Blogs

Book Recommendations