Une ligne de code inutile, et le programme accélère de quatre fois

Une ligne de code inutile, et le programme accélère de quatre fois

CPUCompilateurOptimisation des PerformancesPrédiction de BranchementPrincipes de Bas Niveau

Sources:Lobsters + web research

Ajouter une seule ligne de code, et le programme non seulement ne ralentit pas, mais gagne en vitesse par un facteur de quatre. Cela ressemble à une légende urbaine, et pourtant, le 12 juillet 2026, un programmeur nommé purplesyringa a relaté sur son blog ce qu’il avait lui-même vérifié de ses propres yeux.

Il était alors en train d’écrire un programme de compression de données. Le cœur du programme tenait en une boucle très courte, réduite à une seule ligne essentielle — consulter à répétition une table pour en extraire la valeur suivante, puis stocker ce qui venait d’être trouvé. Une seule phrase, nette et propre. Mais le programme était d’une lenteur exaspérante à l’exécution. Il a essayé toutes sortes d’optimisations classiques, sans grand effet. Jusqu’à ce qu’il fasse quelque chose qu’il trouvait lui-même absurde : ajouter un if en apparence totalement superflu — un test qui vérifie 「si la nouvelle valeur trouvée est identique à la valeur courante」, et ne met à jour que si elles diffèrent, sinon saute l’opération.

Le degré d’inutilité de cet if équivaut à peu près à ceci : vous savez déjà que vous avez cent euros dans votre poche, mais vous y plongez tout de même la main pour vérifier qu’ils y sont vraiment, avant de sortir. Avec ou sans ce geste, vous avez cent euros dans la poche. Et pourtant, le miracle : après l’ajout, le programme est passé de 320 microsecondes à 80 microsecondes d’exécution — quatre fois plus rapide, pile.

La première fois que j’ai lu ce cas, je l’ai moi aussi pris pour une blague. Mais ce n’est pas de la magie noire. Derrière tout ça se cache une histoire sur la façon dont les ordinateurs modernes 「devinent」 la réponse.

Le goulot d’étranglement sur la chaîne de montage

Pour comprendre ce qui se passe, il faut d’abord savoir comment un CPU s’y prend pour travailler.

Imaginez le CPU comme une chaîne de montage d’usine. Les ouvriers sur la chaîne n’attendent pas qu’un produit soit entièrement assemblé avant de commencer le suivant — ce serait bien trop lent. Ils découpent le travail en de nombreuses petites étapes : découpe, ponçage, peinture, contrôle qualité… chaque poste traite simultanément un produit différent. De la sorte, la cadence de toute la ligne dépend 「du poste le plus lent」, et non de 「faire un produit après l’autre」. C’est ce qu’on appelle le parallélisme au niveau des instructions des CPU modernes — traiter plusieurs instructions en parallèle pour gagner massivement en efficacité.

Mais la chaîne de montage a un point faible mortel : si le produit suivant dépend du produit précédent, dont on ne connaît le résultat qu’une fois l’assemblage terminé, toute la chaîne se bloque. Les ouvriers n’ont plus qu’à attendre, les bras ballants.

Dans le code de purplesyringa, c’est exactement cette situation. Sa boucle était : j = next_j[i][j] — utiliser la valeur courante j pour consulter la table, en tirer le j suivant, puis reprendre ce nouveau j pour la prochaine itération. Chaque tour dépend du résultat du tour précédent. Les ouvriers de la chaîne du CPU attendent avec angoisse que le poste d’avant livre sa sortie, et ce poste attend lui-même celui d’encore avant… toute la ligne se transforme en un embouteillage sur une route à sens unique. C’est ce qu’on appelle le goulot d’étranglement de latence causé par une 「chaîne de dépendances de données」.

Un système de navigation qui « devine » le chemin

Mais les CPU modernes possèdent un tour de force précisément adapté à cette situation. On l’appelle le « branch predictor ».

Reprenons la métaphore de l’usine : sur la chaîne se trouve un poste de contrôle qualité ; selon le résultat du contrôle, l’ouvrier décide si le produit prend la voie A ou la voie B. S’il attendait à chaque fois la fin du contrôle pour choisir la voie, la chaîne se bloquerait encore. L’usine a donc installé un 「système d’expérience historique」 — à chaque passage à ce poste, le système devine, d’après les 99 choix passés, que 「cette fois-ci, ce sera encore très probablement la voie A」. L’ouvrier pousse le produit vers la voie A à l’avance. S’il a deviné juste, la chaîne coule sans jamais s’arrêter ; s’il s’est trompé, il ramène les demi-produits déjà engagés dans la voie A et refait le tour par la voie B.

Le branch predictor du CPU, c’est exactement ce système. Il enregistre les choix passés du programme à chaque 「carrefour」, puis utilise un circuit complexe pour prédire la direction du prochain. Les CPU modernes atteignent généralement une précision de prédiction de branchement supérieure à 95 % — plus précis que la plupart des décisions prises par un être humain.

Le coup de génie de purplesyringa réside ici : bien que son code ne contienne aucun 「carrefour」 évident (pas de if-else), la chaîne de dépendances de données est en elle-même une 「attente」 invisible. Une intuition lui vient : et si l’on introduisait un carrefour explicite, juste pour faire intervenir le branch predictor ?

Le véritable rôle de cette ligne « inutile »

L’if qu’il a ajouté obéit à cette logique : vérifier si le résultat de la table diffère de la valeur courante, et ne mettre à jour que s’ils sont différents (sinon, ne rien faire). Comme dans l’écrasante majorité des cas la valeur trouvée est bien identique à la valeur courante, le branch predictor du CPU 「apprend」 très vite : le corps de cet if est presque jamais exécuté.

Le CPU parie donc hardiment : au tour suivant, on saute encore le corps de l’if. Puisqu’il pari sur le saut, il n’a plus besoin d’attendre le résultat du tour précédent — il suppose directement que j n’a pas changé, et continue de foncer. La chaîne se remet en mouvement. Plusieurs tours de boucle peuvent être traités en parallèle.

Lorsque, quelques rares fois, le résultat de la table diffère réellement, le branch predictor se rend compte de son erreur, purge les demi-produits engagés sur la mauvaise voie et refait ce tour avec la bonne valeur de j. Ce processus s’appelle la 「branch misprediction penalty」 (pénalité d’échec de prédiction de branchement). Mais comme la proportion d’erreurs est extrêmement faible, ce coût reste bien inférieur à celui d’attendre en permanence.

Le résultat : une instruction if en apparence totalement superflue a fourni au branch predictor un signal qui dit 「je peux deviner」. Elle a transformé une chaîne de dépendances qui ne pouvait être exécutée qu’en série en une chaîne de montage où l’on peut spéculer et exécuter en parallèle.

Quand la « bonne volonté » du compilateur fait déraper

L’histoire n’est racontée qu’à moitié. Il y a un adversaire encore plus corsé : le compilateur.

Le compilateur est le programme chargé de traduire le code lisible par l’humain écrit par le programmeur en instructions machine que le CPU peut exécuter. Les compilateurs modernes sont extrêmement intelligents — si intelligents qu’ils repèrent automatiquement le 「code mort」 et le suppriment purement et simplement. Aux yeux du compilateur, l’if ajouté par purplesyringa disait en substance 「si A est différent de A, alors mets à jour A」, ce qui est évidemment du nonsense. Le compilateur ricane et l’optimise au rebut.

Le programmeur voulait duper le branch predictor du CPU, mais le compilateur a confisqué l’accessoire de la duperie avant même qu’il ne s’en serve.

Voilà le sens du 「choix conservateur」 mentionné dans le titre — et c’est aussi, à mon sens, le point le plus savoureux de ce cas : le compilateur respecte strictement le principe 「ne pas modifier la sémantique du programme」 — ce que vous écrivez est logiquement inutile, je ne vous le traduis pas. Mais ce que le compilateur ignore, c’est que la véritable valeur de certains codes réside au niveau matériel : ils fournissent au CPU un signal lui permettant une exécution spéculative.

Il s’agit en réalité d’un jeu à trois. Le CPU est l’agressif : il devine à tout prix, cherche par tous les moyens à faire le travail en avance. Le compilateur est le conservateur : il respecte strictement la sémantique, ne fait ni plus ni moins. Et le programmeur se tient au milieu, voulant à la fois exploiter l’agressivité du CPU et tromper la prudence du compilateur.

Le scellé 「ne touche pas à ça」

La solution trouvée par purplesyringa consiste à utiliser un mot-clé du C appelé volatile. Dans le langage C, ce mot équivaut à coller sur une variable une étiquette 「ne touche pas à ça」 — il dit au compilateur : ces données peuvent changer à votre insu, donc ne les optimisez pas, relisez-les à chaque fois, sagement.

Une fois ce scellé apposé, le compilateur ne considère plus la condition de l’if comme un nonsense 「toujours faux」, et la conserve. L’if est sauf ; le branch predictor a de quoi deviner ; la chaîne de montage peut de nouveau tourner en parallèle.

Plus tard, dans la discussion sur la communauté Lobsters, un autre programmeur, ibookstein, a découvert que l’utilisation de l’annotation [[unlikely]] du C++20 (qui revient à dire explicitement au compilateur 「cette branche est rarement prise」) produit un effet similaire. purplesyringa a toutefois souligné que la méthode du scellé volatile génère un meilleur code machine, et qu’elle n’est pas limitée à un compilateur particulier.

Un concept plus vaste : la value speculation

Dans le fil de discussion Lobsters, quelqu’un a fait remarquer que cette astuce porte en réalité un nom officiel — la 「value speculation」. L’idée centrale : lorsque nous disposons d’une heuristique 「très probablement juste」 pour deviner la valeur d’un élément, on peut s’appuyer sur le branch predictor pour exécuter de façon spéculative, et ainsi briser la chaîne de dépendances de données.

Ce concept remonte à des recherches et blogs antérieurs (les travaux de Paul Khuong, Per Vognsen, entre autres). Dans un article classique de mazzo.li, la même astuce sert à accélérer le parcours d’une liste chaînée : lors du parcours, l’adresse du nœud suivant dépend du pointeur stocké dans le nœud courant, ce qui constitue encore une chaîne de dépendances de données. Mais si l’on devine 「le nœud suivant se trouve en mémoire juste à côté du nœud courant」, on peut demander au CPU de précharger à l’avance, faisant passer le débit de 14 Go/s à 45 Go/s (lorsque les données résident dans le cache du CPU).

La technique de l’if par purplesyringa et la value speculation sont au fond la même chose : substituer une devinette bon marché à une longue et coûteuse attente.

Ce qui se dresse contre vous

Le plus fascinant dans cette affaire, c’est qu’elle révèle trois niveaux de conflit entre 「ce que vous croyez」 et 「la réalité」 :

Premier niveau : l’intuition humaine veut que 「moins de code, c’est plus rapide」. Mais dans ce cas, ajouter une ligne a rendu le programme plus rapide — car cette ligne a pour fonction d’émettre un signal, non d’effectuer un calcul.

Deuxième niveau : le compilateur estime que 「le code logiquement inutile doit être supprimé」. Mais l’utilité de certains codes se niche dans le comportement matériel, non dans la sémantique logique.

Troisième niveau : nous pensons généralement que 「se tromper coûte cher, donc autant ne pas deviner」. Or la philosophie de conception des CPU modernes est précisément l’inverse : devinez hardiment, si vous devinez juste vous gagnez, si vous vous trompez il suffira de tout rebâtir. Tant que la probabilité de deviner juste est assez élevée, l’ensemble reste gagnant.

Cette histoire n’a pas de grande conclusion à portée universelle. Ce n’est qu’un programmeur, en optimisant un algorithme de compression, qui a heurté par hasard un fait contre-intuitif. Mais à travers cette petite ligne de if, on entrevoit une vérité subtile au plus profond de l’ordinateur moderne : le CPU est un joueur, le compilateur est un avocat, et le meilleur des programmeurs est souvent celui qui sait quand tromper l’avocat pour transmettre l’information au joueur.


Sources

  • Blog de Purplesyringa : Quadrupling code performance with a “useless” if (12 juillet 2026, l’article original documente l’intégralité des détails techniques, des exemples de code et des données de performance)
  • Discussion sur la communauté Lobsters (s/1an425) : 104 points, 14 commentaires, incluant la solution de remplacement [[unlikely]] découverte par ibookstein, ainsi que le lien vers le concept de 「value speculation」 signalé par mikejsavage
  • mazzo.li : Beating the L1 cache with value speculation (juillet 2021, présente en détail l’application de la value speculation au parcours de listes chaînées, accompagnée de graphiques comparatifs de performance)