Introduction : Pourquoi l'ordinateur « pense-t-il » plus vite que le programmeur ?
\n\nImaginez : vous avez passé votre soirée à écrire sur un papier la séquence de Collatz pour le nombre 27. Puis votre collègue a écrit en C++ une boucle qui a parcouru un milliard de nombres en une seconde. Vous êtes furieux ? Attendez, ce n'est que le début.
\n\nEn 2025, sur Stack Overflow, une vieille question très révélatrice a refait surface avec une vigueur renouvelée : "Why does C++ code for testing the Collatz conjecture run faster than hand-written assembly?". Apparemment, l'assembleur est un dialogue pur avec le processeur, sans intermédiaire. Comment se fait-il que le C++ de haut niveau surpasse l'assembleur écrit par un humain ?
\n\nAnalysons ce paradoxe à l'aide de la conjecture de Collatz (également connue sous le nom de problème 3n+1). Vous découvrirez pourquoi les compilateurs C++ modernes sont de véritables machines à remonter le temps, et comment écrire en 2025 un code plus rapide que la gestion « manuelle » des registres.
\n\nQu'est-ce que la conjecture de Collatz et quel est le rapport avec le C++ ?
\n\nRappelons le principe : prenons n'importe quel nombre naturel n. S'il est pair, on le divise par 2. S'il est impair, on le multiplie par 3 et on ajoute 1. La conjecture affirme que tout nombre finira par atteindre 1. La tâche du programmeur est de vérifier cela pour la plus grande plage de nombres possible.
Une implémentation classique en C++ ressemble à ceci :
\n\n#include <iostream>\n\nint collatz_steps(long long n) {\n int steps = 0;\n while (n != 1) {\n if (n % 2 == 0) {\n n /= 2;\n } else {\n n = 3 * n + 1;\n }\n ++steps;\n }\n return steps;\n}\n\nint main() {\n long long max_steps = 0;\n long long best_num = 0;\n for (long long i = 1; i <= 1000000; ++i) {\n int s = collatz_steps(i);\n if (s > max_steps) {\n max_steps = s;\n best_num = i;\n }\n }\n std::cout << "Nombre : " << best_num << " étapes : " << max_steps;\n}\n\n\nRien de bien compliqué en apparence. Mais c'est précisément ce code simple qui a donné naissance à un débat légendaire avec l'assembleur.
\n\nPourquoi l'assembleur perd-il ? Les principales erreurs de 2025
\n\nLorsqu'un développeur écrit en assembleur, il pense souvent : « Je sais comment fonctionne le processeur, je vais écrire le code parfait ». Mais les processeurs modernes ne sont pas de simples exécuteurs d'instructions. Ce sont des pipelines superscalaires complexes avec prédiction de branchement, exécution spéculative et mémoire cache. Le compilateur C++ (GCC, Clang, MSVC) connaît tout de son architecture cible.
\n\nErreur n°1 : Ignorer l'optimisation des branchements
\n\nEn assembleur manuel, le programmeur écrit souvent les branchements « à la va-vite » :
\n; Pseudo-assembleur\ncheck_even:\n test rax, 1\n jnz is_odd\n shr rax, 1\n jmp next\nis_odd:\n mov rbx, rax\n shl rax, 1\n add rax, rbx\n inc rax\nnext:\n ...\n\nLe compilateur C++ avec le flag -O3 fait plus malin. Il utilise CMOV (conditional move) et de la magie binaire. Au lieu d'un branchement, il utilise de l'arithmétique :
; GCC 14 générera quelque chose comme :\n mov rcx, rax\n shr rcx, 1 ; division par 2 (si pair)\n and eax, 1 ; vérification du bit de poids faible\n lea rdx, [rax + rcx*2 + 1] ; calcul astucieux pour l'impair\n test eax, eax\n cmove rax, rcx ; si pair - on prend la division\n cmovne rax, rdx ; si impair - on prend 3n+1\n\n\nRésultat : pas de stall de pipeline dû à des branchements mal prédits. En 2025, c'est crucial, car le prédicteur de branchement dans les CPU est devenu plus intelligent, mais reste imparfait pour les nombres aléatoires.
\n\nErreur n°2 : Oublier la vectorisation (AVX-512)
\n\nLes compilateurs C++ modernes (notamment Clang 18+ et GCC 14+) vectorisent automatiquement les boucles. Si vous écrivez une simple boucle de vérification, le compilateur peut traiter jusqu'à 8 nombres par cycle d'horloge, en utilisant