Principe et mise en œuvre
Le principe push‑ifs‑up‑fors‑down recommande de placer les if le plus tôt possible dans la chaîne d’appel et de reporter les boucles for à l’intérieur d’une fonction qui traite un lot de données. En pratique, le code qui reçoit un Option<T> doit être remplacé par une fonction qui ne prend que T, le test None étant effectué par l’appelant. Cette transformation réduit le nombre de branches exécutées dans la fonction critique, ce qui améliore la prédictibilité du pipeline CPU et diminue le taux de miss de la prédiction de branche.
let maybe_walruses: Vec<Option<Walrus>> = ...;
let walruses: Vec<Walrus> = maybe_walruses
.into_iter()
.filter_map(|w| w)
.collect();
frobnicate_batch(&walruses); // aucune branche interneLe second avantage provient de la possibilité de vectoriser la boucle interne. Une fonction frobnicate_batch travaille sur un tableau contigu, ce qui permet au compilateur d’émettre des instructions SIMD et de réduire le coût d’appel par itération. Le nombre d’instructions de contrôle passe de n (une branche par élément) à 1 (une branche par lot).
Analogies en bases de données
Les optimisateurs SQL appliquent la même logique : les projections (SELECT col1, col2) et les sélections (WHERE) sont « poussées vers le bas », c’est‑à‑dire exécutées dès les scans de tables. Cette précocité filtre les tuples avant que les opérations coûteuses, comme les JOIN, ne soient invoquées. Le coût de chaque JOIN dépend du nombre de lignes d’entrée ; en réduisant ce nombre, on diminue le temps de combinaison quadratique ou hash‑based.
Par ailleurs, les moteurs modernes offrent une exécution vectorisée (batch) où chaque opérateur traite des blocs de milliers de tuples. Le modèle « volcano » invoque next() par tuple, introduisant une surcharge de contrôle comparable à un if par ligne. La version batch correspond à la fonction frobnicate_batch du code Rust : la surcharge de décision est amortie sur le lot, ce qui améliore la localisation du cache et la bande passante mémoire.
Interprétation catégorique et limites
En théorie des catégories, un prédicat p : A → Bool définit un sous‑objet {a ∈ A | p(a)} avec une monomorphie d’inclusion {a | p(a)} ↪ A. L’opération « push‑ifs‑up » correspond à factoriser la fonction f : A → B qui teste p en interne. Après factorisation, l’appelant gère le cas ¬p et transmet uniquement les éléments du sous‑objet à f. Dans le langage Rust, Option<Walrus> représente le coproduit 1 + Walrus. La fonction qui consomme Option<Walrus> se décompose en une paire de fonctions, l’une pour le cas None, l’autre pour Walrus. En poussant le test, on élimine le premier composant et on conserve uniquement la seconde, ce qui se traduit par un type plus restreint et une implémentation sans branche.
Cette approche n’est pas universelle. Si le coût du filtrage préalable dépasse le gain de vectorisation, ou si le filtre dépend de données qui ne sont disponibles qu’après un JOIN, le déplacement des if peut introduire une surcharge de copie ou de transformation. De même, dans des systèmes fortement parallélisés, la granularité du lot doit être adaptée : un lot trop grand peut entraîner une latence accrue, tandis qu’un lot trop petit ne profite pas pleinement des instructions SIMD.