Faire remonter les `if`, faire descendre les `for`
(matklad.github.io)- Faire remonter les instructions
ifvers le site d’appel à l’intérieur d’une fonction aide à réduire la complexité du code - En centralisant les tests de condition et le traitement des branches à un seul endroit, il devient plus facile de repérer les duplications et les vérifications de branchement inutiles
- Le refactoring de dissolution d’enum permet d’éviter que la même condition se disperse à plusieurs endroits du code
- Les boucles
forfondées sur des opérations par lot sont efficaces pour améliorer les performances et optimiser les traitements répétitifs - La combinaison du modèle les
ifvers le haut, lesforvers le bas permet d’améliorer à la fois la lisibilité et l’efficacité du code
Brève note sur deux règles liées
- Lorsqu’une fonction contient une condition
if, il est recommandé de se demander si elle peut être déplacée vers le site d’appel - Comme dans l’exemple, plutôt que de vérifier une precondition à l’intérieur de la fonction, il est préférable de confier cette vérification à l’appelant, ou de faire en sorte que le type (ou un
assert) garantisse la precondition - Cette approche de remontée des vérifications de precondition a un effet global sur le code et permet, dans l’ensemble, de réduire le nombre de tests de condition inutiles
Concentrer le flux de contrôle et les conditions
- Le flux de contrôle et les instructions
ifsont des causes majeures de complexité et de bugs dans le code - Il est utile de concentrer les branchements plus haut, par exemple au niveau de l’appelant, de sorte qu’une seule fonction gère la logique de branchement, tandis que le vrai travail est confié à des sous-routines linéaires
- Quand les branchements et le flux de contrôle sont regroupés à un seul endroit, il devient plus facile d’identifier les branches dupliquées et les conditions inutiles
Exemple :
- Lorsqu’il y a des
ifimbriqués dans une fonctionf, il est plus facile de repérer du code mort (Dead Branch) - Si les branchements sont dispersés entre plusieurs fonctions (
g,h), cela devient plus difficile à voir
Refactoring de dissolution d’enum (Dissolving enum Refactor)
- Quand le code encapsule les mêmes branchements conditionnels dans un
enumou équivalent, on peut faire remonter la condition vers un niveau supérieur pour séparer plus clairement la logique de branchement du travail lui-même - Cette approche évite que la même condition apparaisse plusieurs fois dans le code
Exemple :
- Une situation où la même condition de branchement est exprimée à la fois dans les fonctions
f,get dans l’enum E - Peut être simplifiée dans l’ensemble du code avec un unique branchement conditionnel de niveau supérieur
Pensée orientée données (Data Oriented Thinking) et opérations par lot
- La plupart des programmes fonctionnent avec de nombreux objets (entités). Sur le chemin critique (
Hot Path), les performances dépendent du traitement d’un grand nombre d’objets - Il est souhaitable d’introduire la notion de lot (
batch) et de faire des opérations sur des ensembles d’objets le cas général, tandis que l’opération sur un seul objet devient un cas particulier
Exemple :
-
Prendre comme base une fonction de traitement par lot comme
frobnicate_batch(walruses) -
Et convertir le traitement d’un objet individuel en cas particulier via une boucle
for -
Cette approche joue un rôle important pour l’optimisation des performances : sur des traitements massifs, elle réduit le coût de démarrage et augmente la flexibilité d’ordonnancement
-
Elle permet aussi d’exploiter des opérations SIMD (
struct-of-array, etc.), par exemple en traitant certains champs en bloc avant l’opération globale
Cas pratiques et modèles recommandés
- Comme pour la multiplication de polynômes basée sur la FFT, le fait de pouvoir calculer simultanément en plusieurs points permet de maximiser les performances
- La règle qui consiste à faire remonter les conditions et descendre les boucles peut être appliquée conjointement
Exemple :
- Plutôt que de retester la même condition à chaque itération dans une boucle, sortir la condition de la boucle réduit les branchements dans la boucle et facilite l’optimisation et la vectorisation
- Cette approche garantit aussi une grande efficacité dans les plans de données de systèmes à grande échelle, comme dans la conception de TigerBeetle
Conclusion
- En combinant le modèle où les
if(conditions) remontent vers le haut (site d’appel, logique de contrôle) et lesfor(boucles) descendent vers le bas (logique de calcul, traitement des données), on améliore à la fois la lisibilité, l’efficacité et les performances du code - Réfléchir en termes d’espace vectoriel abstrait, c’est-à-dire d’opérations sur des ensembles, est un meilleur outil de résolution de problèmes que de traiter sans cesse des branchements répétitifs
- En résumé : les
ifvers le haut, lesforvers le bas !
1 commentaires
Commentaires Hacker News
ifà chacun de ces appels ? Peut-on vraiment demander de déplacer ainsi leifpour des fonctions commegetaddrinfoouEnterCriticalSection? À mon avis, ce type de transformation n’est à envisager que pour des fonctions appelées à un ou deux endroits, et lorsque cette décision ne relève pas de la responsabilité de la fonction. Une approche consiste à écrire une fonction qui ne fait que la condition et délègue à une fonction helper. Et si l’on doit déplacer la condition hors de la boucle, on peut faire en sorte que l’appelant utilise directement un helper de plus bas niveau pour la condition. Mais au fond, toute cette réflexion tourne autour de « l’optimisation ». Or l’optimisation entre souvent en conflit avec une meilleure conception du programme. Il peut être préférable que l’appelant n’ait pas besoin de connaître la condition. On retrouve souvent ce dilemme en POO : la décision, représentée ici parif, est en réalité prise via le dispatch de méthode. Sortir ce dispatch de la boucle peut aussi entrer en tension avec les principes de conception. Par exemple, pour dessiner une image sur un canvas, mieux vaut utiliser une méthode commeblitque d’appelerputpixelen boucleEnterCriticalSectiondoit faire une validation solide dès le point d’entrée, y compris via des conditions. En revanche, dans du code applicatif, on peut déplacer leifvers l’appelant. Dans une bibliothèque ou du code central, il est approprié de pousser le flux de contrôle vers les bords. Dans son propre domaine, il est bon de garder le flux de contrôle sur les bords. Mais ce genre de règle reste toujours idiomatique ; au final, il faut quelqu’un capable de juger raisonnablement selon le contextematchpar un appel de méthode polymorphe. L’objectif est de séparer le moment où l’on décide de la branche initiale et le moment où l’action réelle est exécutée. La distinction entre les cas est portée par l’objet lui-même (ici la valeur de l’enum) ou par une closure, donc il n’est pas nécessaire de la répéter à chaque appel. Si la distinction entre les cas change, il suffit de modifier le point de branchement, sans toucher aux endroits où le comportement se produit réellement. L’inconvénient est un compromis entre la commodité de voir directement le comportement de chaque cas et le fait d’introduire au niveau du code une dépendance à la liste des casif. Pourtant, cet article recommande l’inverse : remonter lesif, c’est-à-dire vers des fonctions de plus haut niveau. Cela permet de centraliser dans une seule fonction la logique de branchement complexe, tout en déléguant le travail concret à des sous-routinesif (weShouldDoThis()) { doThis(); }. Si l’on extrait chaque vérification dans une fonction distincte, il devient plus facile de tester et de gérer la complexitéifdans une boucleforpeut être plus facile à gérer qu’unifà l’extérieur de la boucle, et offrir une meilleure efficacité d’accès mémoire. Pour prendre un exemple concret, s’il faut faire+1pour les nombres impairs et-2pour les nombres pairs, il faudrait normalement brancher à chaque itération, mais avec du SIMD vectoriel on peut traiter 16intà la fois, sans branchement. Si le compilateur vectorise correctement, il transformera le code d’origine en une version optimisée sans branchementbeforeproposé me semble un peu à côté du sujet de l’article ; au contraire, la version SIMD optimisée correspond précisément à son propos. Dans l’exemple, leifà l’intérieur de la boucle dépend des données et ne peut donc pas être facilement remonté. Si l’algorithme utilisait au contraire une condition extérieure à la boucle, commeif (length % 2 == 1) { ... } else { ... }, alors oui, ce genre de condition doit évidemment être déplacé au-dessus dufor. Dans la version SIMD, leifa complètement disparu, et c’est le genre de pattern idéal que l’auteur de l’article apprécierait sans doutefor. Est-ce que quelqu’un sait à quel point il est difficile pour un compilateur d’auto-vectoriser ce type de code ? La frontière m’intéresseforcrée une abstraction du type « leforest la règle, la branche est le comportement ». Mais quand de nouvelles exigences apparaissent, cette abstraction se brise, et l’on finit par ajouter de force des paramètres ou des exceptions, ce qui rend le code difficile à comprendre. Si l’on avait écrit le code sans abstraction dès le départ, le résultat aurait sans doute été plus clair et plus facile à maintenir. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction