Les zero-cost abstractions sont une idée clé de Rust, signifiant que les abstractions (par exemple, les types génériques, les itérateurs, les traits) ne devraient pas entraîner de coûts en temps ou en mémoire par rapport à l'écriture manuelle d'un code équivalent. Autrement dit, le code de haut niveau, une fois optimisé, se compile aussi efficacement que le code de bas niveau.
Rust y parvient grâce au mécanisme de monomorphisation : le code générique est compilé pour chaque type spécifique séparément, sans appels dynamiques. Les itérateurs et les fermetures écrits avec des traits/génériques sont, après optimisation, divulgués par le compilateur dans un code typé rigide direct, où toutes les enveloppes superflues sont éliminées.
Exemple d'itérateur zéro-coût :
let v = vec![1,2,3]; let sum: i32 = v.iter().map(|x| x * 2).sum();
Ce fragment après optimisation se transforme presque en une boucle manuelle :
let mut sum = 0; for x in &v { sum += x * 2; }
Cela signifie-t-il que l'abstraction sans coût en Rust, lors de l'utilisation d'objets traits (par exemple, &dyn Trait), n'entraînera pas de surcharge à l'exécution ?
Réponse : Non ! La surcharge à l'exécution apparaît lors du choix dynamique de la méthode via vtable - lorsque dyn Trait est utilisé au lieu de fonctions génériques. Zero-cost est uniquement obtenu avec des abstractions génériques (monomorphisées) statiques.
Exemple :
trait Speaker { fn speak(&self); } fn say_twice<T: Speaker>(v: T) { v.speak(); v.speak(); } fn say_twice_dyn(v: &dyn Speaker) { v.speak(); v.speak(); } // Le premier appel est monomorphisé, le second - via vtable
Histoire
Histoire
Histoire
Dans une bibliothèque tierce, une structure générique a été créée et retournée comme Box<dyn Trait>. Malgré une implémentation générique, nous avons perdu le zero-cost, car nous avons déplacé l'abstraction à l'exécution.