Quiz : Gestion essentielle de la mémoire en Rust
(Borrow) checke‑toi avant de te crasher ! 🦀
Prêt·e à mettre à l’épreuve vos compétences en gestion de mémoire Rust ? 🦀
Ce quiz mettra à l’épreuve votre compréhension du système de possession, des règles d’emprunt, des durées de vie et des pointeurs intelligents de Rust.
Note : Les questions sont formatées sur une largeur d’environ 50 colonnes afin de garantir une lisibilité sur tous les appareils. (Les suggestions d’amélioration sont les bienvenues !)
Que vous soyez un·e Rustacean chevronné·e ou que vous débutiez tout juste avec la gestion de mémoire, ce quiz vous aidera à consolider vos connaissances. Allons-y ! 🦀
Que se passe-t-il lorsque vous exécutez ce code ? Essayez de prédire la sortie ou l’erreur :
fn main() { let philosopher = String::from("Zeno of Citium"); let greeting = philosopher;
println!("Hello, {}!", philosopher);}Ce code ne compile pas à cause des règles de propriété de Rust. Lorsque nous assignons philosopher à greeting, la propriété du String est déplacée vers greeting. Après ce déplacement, philosopher n’est plus valide.
Voici trois façons de corriger cela :
- Cloner la chaîne (crée une nouvelle copie) :
let greeting = philosopher.clone();- Utiliser une référence (emprunte la valeur) :
let greeting = &philosopher;- Utiliser une tranche de chaîne (emprunte une partie de la chaîne) :
let greeting = &philosopher[..];Chaque solution a des cas d’utilisation et des implications de performance différents. Le clonage est plus coûteux mais vous donne la propriété, tandis que les références sont moins chères mais soumises à des contraintes de durée de vie.
Que se passe-t-il lorsque vous exécutez ce code ? Pensez au transfert de propriété :
fn take_knowledge(knowledge: String) { println!("Knowledge: {}", knowledge);}
fn main() { let wisdom = String::from("know thyself"); take_knowledge(wisdom); // What happens to our wisdom? println!("Do you {}", wisdom);}Le code ne compile pas parce que la propriété de wisdom a été déplacée vers take_knowledge et ne peut donc plus être utilisée ensuite.
Voici trois façons de résoudre ce problème :
- Passer par référence (emprunter la valeur) :
fn borrow_it(text: &String) { println!("Inside: {}", text);}borrow_it(&wisdom); // Now wisdom can be used after- Cloner la valeur (créer une nouvelle copie) :
take_knowledge(wisdom.clone()); // Original wisdom remains valid- Retourner la propriété depuis la fonction :
fn take_and_return(text: String) -> String { println!("Inside: {}", text); text // Return ownership back}let wisdom = take_and_return(wisdom); // Reassign returned ownershipChaque approche a des cas d’utilisation différents :
- Références : les plus efficaces, mais nécessitent une gestion des durées de vie
- Clonage : simple mais potentiellement coûteux
- Retour de propriété : utile pour transformer des valeurs
Bonne pratique : utilisez les références sauf si vous avez besoin d’un transfert de propriété.
Que se passe-t-il avec plusieurs références mutables ?
fn main() { let mut wisdom = String::from("He who laughs at"); let ref1 = &mut wisdom; // First mutable borrow let ref2 = &mut wisdom; // Second mutable borrow ref1.push_str(" himself never runs"); ref2.push_str(" out of things to laugh at.");}Réfléchissez aux règles de Rust concernant les références mutables.
Ce code viole les règles fondamentales d’emprunt de Rust :
- Une SEULE référence mutable à une valeur à la fois
- OU n’importe quel nombre de références immuables
- Les références ne peuvent pas survivre à leur référent
Voici comment corriger le code :
- Utiliser un scoping séquentiel :
let mut wisdom = String::from("He who laughs at");{ let ref1 = &mut wisdom; ref1.push_str(" himself never runs");} // ref1 goes out of scopelet ref2 = &mut wisdom; // Now this is validref2.push_str(" out of things to laugh at.");- Ou modifier la chaîne dans un emprunt unique :
let mut wisdom = String::from("He who laughs at");let ref1 = &mut wisdom;ref1.push_str(" himself never runs out of things to laugh at.");Ces règles empêchent les courses de données à la compilation, rendant Rust sûr pour le multithreading par défaut.
Piège fréquent : essayer d’utiliser plusieurs références mutables pour éviter le clonage ou pour modifier différentes parties de la même valeur simultanément.
Ce code compile‑t‑il ? Si oui, pourquoi ? Sinon, qu’est‑ce qui ne va pas ?
fn first_word(s: &str) -> &str { // No explicit lifetimes? match s.find(' ') { Some(pos) => &s[0..pos], None => s, }}
fn main() { let name = String::from("Seneca the Younger"); let first = first_word(&name); println!("Hello, {}", first);}Ce code compile avec succès grâce aux règles d’élision des durées de vie de Rust. Ces règles permettent au compilateur d’inférer automatiquement les durées de vie dans les motifs courants.
Les trois règles d’élision sont :
- Chaque paramètre reçoit son propre paramètre de durée de vie
- S’il n’y a qu’un seul paramètre d’entrée, cette durée de vie est attribuée à tous les paramètres de sortie
- S’il y a plusieurs paramètres d’entrée, mais que l’un d’eux est &self ou &mut self, la durée de vie de self est attribuée à tous les paramètres de sortie
Cette fonction est équivalente à :
fn first_word<'a>(s: &'a str) -> &'a str { // ... same implementation}Motifs courants où l’élision fonctionne :
// These don't need explicit lifetimesfn get_str(s: &str) -> &str { s }fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimesfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}Bonne pratique : laissez l’élision faire son travail quand c’est possible, mais comprenez quand des durées de vie explicites sont nécessaires.
Qu’est-ce qui ne va pas avec cette définition de type récursif ?
#[derive(Debug)]enum CatList { Cons(i32, CatList), // Recursive without indirection Nil,}
fn main() { let catlist = CatList::Cons(1, CatList::Cons(2, CatList::Cons(3, CatList::Nil)));}Ce code échoue parce que le compilateur ne peut pas déterminer la taille de CatList à la compilation. La nature récursive du type signifie qu’il pourrait être infiniment grand !
Voici comment le corriger en utilisant Box<T> :
#[derive(Debug)]enum CatList { Cons(i32, Box<CatList>), // Box provides a fixed-size pointer Nil,}
fn main() { let catlist = CatList::Cons(1, Box::new(CatList::Cons(2, Box::new(CatList::Cons(3, Box::new(CatList::Nil))))));}Pourquoi Box<T> fonctionne :
- Box fournit un pointeur de taille fixe (généralement 8 octets sur les systèmes 64 bits)
- Les données réelles sont stockées sur le tas
- Le compilateur sait maintenant exactement quel espace allouer
Cas d’utilisation courants de Box<T> :
- Structures de données récursives (listes chaînées, arbres)
- Grandes données que vous voulez garantir d’être allouées sur le tas
- Objets de trait lorsque vous avez besoin d’un dispatch dynamique
Meilleure pratique : utilisez Box<T> lorsque vous avez besoin de :
- Types récursifs
- Garantir l’allocation sur le tas
- Déplacer de grandes données sans les copier
Que affichera ce code ? Comptez attentivement !
use std::rc::Rc;
fn main() { let text = Rc::new(String::from("Meditations")); // Count: 1 let marcus = Rc::clone(&text); // What happens here? let aurelius = Rc::clone(&text); // And here? println!( "Reference count: {}", Rc::strong_count(&text) );}Décomposons le fonctionnement de Rc :
- Création initiale avec
Rc::new(): compteur = 1 - Premier clone pour
marcus: compteur = 2 - Second clone pour
aurelius: compteur = 3
Caractéristiques importantes de Rc :
use std::rc::Rc;
fn demonstrate_rc() { let original = Rc::new(String::from("Shared")); println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{ let copy = Rc::clone(&original); println!("Count inside scope: {}", Rc::strong_count(&original)); // 2 } // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1}Points clés :
Rc::clone()est peu coûteux – il ne fait qu’incrémenter un compteur- Rc n’est destiné qu’aux scénarios mono‑thread
- Lorsque la dernière référence est libérée, les données sont nettoyées
- Utilisez les références
Weakpour éviter les cycles de références
Bonnes pratiques :
- Utilisez Rc quand vous avez besoin d’une possession partagée
- Envisagez
Arcpour les scénarios thread‑safe - Évitez de créer des cycles de références
Cette définition de struct compilera-t-elle ? Pourquoi ou pourquoi pas ?
struct Philosopher { name: &str, // Reference without lifetime quote: &str, // Another reference without lifetime}
fn main() { let phil = Philosopher { name: "Seneca", quote: "Luck happens when preparation meets opportunity", };}Le code échoue parce que les structs contenant des références doivent spécifier des durées de vie. Voici comment le corriger :
// Single lifetime parameterstruct Philosopher<'a> { name: &'a str, quote: &'a str,}
// Or different lifetimes if neededstruct PhilosopherFlex<'n, 'q> { name: &'n str, quote: &'q str,}Modèles courants :
// Own the data insteadstruct PhilosopherOwned { name: String, quote: String,}
// Mixed ownershipstruct PhilosopherMixed<'a> { name: String, // Owned quote: &'a str, // Borrowed}Bonnes pratiques:
- Utilisez des types possédés (String) lorsque vous devez stocker des données indéfiniment
- Utilisez des références lorsque la durée de vie du struct est clairement plus courte que les données
- Envisagez plusieurs paramètres de durée de vie lorsque les références peuvent avoir des durées différentes
- Documentez les relations de durée de vie dans les structures complexes
Que se passe-t-il avec cette fonction qui renvoie le plus long des deux tranches de chaîne ?
fn longest(text1: &str, text2: &str) -> &str { if text1.len() > text2.len() { text1 // Returning a reference, but which lifetime? } else { text2 // Could be this reference instead }}
fn main() { println!("{}", longest( "Seneca the Younger", "Marcus Aurelius" ));}Ce code échoue parce que le compilateur ne peut pas déterminer la relation entre les durées de vie d’entrée et de sortie. Voici pourquoi et comment le corriger :
// Correct version with explicit lifetime annotationfn longest<'a>(text1: &'a str, text2: &'a str) -> &'a str { if text1.len() > text2.len() { text1 } else { text2 }}
// Alternative with different lifetimesfn longest_flex<'a, 'b>(text1: &'a str, text2: &'b str) -> &'a str { if text1.len() > text2.len() { text1 } else { text2.to_string().as_str() // Won't compile! Shows why we need same lifetime }}Pourquoi des durées de vie sont nécessaires ici :
- Plusieurs références d’entrée peuvent avoir des durées de vie différentes
- La valeur de retour doit vivre aussi longtemps que les deux entrées
- Le compilateur doit vérifier ces relations
Modèles courants :
// Single input reference - elision worksfn first_word(s: &str) -> &str { /* ... */ }
// Multiple references, same lifetime neededfn compare_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { /* ... */ }
// Different lifetimes possiblefn combine<'a, 'b>(s1: &'a str, s2: &'b str) -> String { /* ... */ }Bonnes pratiques :
- Laisser l’élision des durées de vie fonctionner quand c’est possible
- Utiliser des durées de vie explicites lorsque les relations doivent être claires
- Envisager de retourner des types possédés pour éviter la complexité des durées de vie
- Documenter les relations de durée de vie complexes
Que se passe-t-il lorsque ce code s’exécute ?
use std::cell::RefCell;
fn main() { let data = RefCell::new(42); let _borrow1 = data.borrow_mut(); // First mutable borrow let _borrow2 = data.borrow_mut(); // Second mutable borrow println!("Value: {}", _borrow2);}RefCell fournit une mutabilité intérieure mais applique toujours les règles d’emprunt de Rust à l’exécution :
use std::cell::RefCell;
fn demonstrate_refcell() { let data = RefCell::new(42);
// Correct way to use RefCell { let mut first = data.borrow_mut(); *first += 1; } // first is dropped here
// Now we can borrow again let second = data.borrow_mut();
// Or multiple immutable borrows let read1 = data.borrow(); let read2 = data.borrow(); // This is OK}Concepts clés :
- RefCell déplace les vérifications d’emprunt à l’exécution
- Peut provoquer des panics si les règles sont violées
- Utile pour le pattern de mutabilité intérieure
Cas d’utilisation courants :
- Objets factices dans les tests
- Implémentation de structures auto‑référentielles
- Lorsque vous devez muter des données derrière une référence partagée
Bonnes pratiques :
- Privilégier l’emprunt à la compilation quand c’est possible
- Garder les emprunts RefCell dans des portées étroites
- Envisager d’utiliser
drop()pour terminer explicitement les emprunts - Utiliser RefCell quand vous avez besoin de mutabilité intérieure
Que affichera ce code ?
use std::cell::Cell;
fn main() { let life = Cell::new(42); let meaning = &life; // Shared reference println!("{}", life.get()); // What prints here? meaning.set(43); // Mutation through shared ref println!("{}", life.get()); // And here?}Cell et RefCell servent des objectifs différents pour la mutabilité intérieure :
use std::cell::{Cell, RefCell};
// Cell for Copy typesstruct Counter { count: Cell<i32>,}
impl Counter { fn increment(&self) { self.count.set(self.count.get() + 1); }}
// RefCell for non-Copy typesstruct Logger { messages: RefCell<Vec<String>>,}
impl Logger { fn log(&self, msg: &str) { self.messages.borrow_mut().push(msg.to_string()); }}Différences clés :
- Cell :
- Fonctionne au mieux avec les types Copy
- Pas d’API d’emprunt
- Copie ou déplace toujours les valeurs
- RefCell :
- Fonctionne avec n’importe quel type
- Possède une API d’emprunt
- Vérification d’emprunt à l’exécution
Bonnes pratiques :
- Utilisez Cell pour les types Copy simples (nombres, bool, etc.)
- Utilisez RefCell quand vous devez emprunter le contenu
- Gardez les mutations via Cell/RefCell au minimum
- Documentez pourquoi la mutabilité intérieure est nécessaire
Quand devez‑vous utiliser Rc (Comptage de références) en Rust ?
Considérez cet exemple :
use std::rc::Rc;
struct SharedConfig { name: String, value: i32,}
fn main() { let config = Rc::new(SharedConfig { name: "settings".to_string(), value: 42, });
let config2 = Rc::clone(&config); // Both config and config2 share ownership}Rc (Comptage de références) est conçu pour les scénarios monothread où vous avez besoin d’une propriété partagée.
Cas d’utilisation courants :
use std::rc::Rc;use std::cell::RefCell;
// Shared ownership in data structuresstruct Node { next: Option<Rc<Node>>, value: i32,}
// Combining with interior mutabilitystruct SharedState { data: Rc<RefCell<Vec<String>>>,}
// Multiple owners of same datalet original = Rc::new(vec![1, 2, 3]);let clone1 = Rc::clone(&original);let clone2 = Rc::clone(&original);Points clés:
- Utilisez Rc lorsque :
- Plusieurs parties de votre code ont besoin de posséder la donnée
- Vous savez que le partage est monothread
- La durée de vie ne peut pas être déterminée statiquement
- Utilisez Arc à la place lorsque :
- Vous avez besoin d’un partage sûr pour les threads
- Plusieurs threads ont besoin de posséder la donnée
- Limitations de Rc :
- Pas sûr pour les threads
- Légère surcharge à l’exécution
- Ne peut pas rompre automatiquement les cycles de références
Bonnes pratiques :
- Privilégiez la propriété unique quand c’est possible
- Utilisez Rc pour un partage monothread
- Utilisez Arc pour les scénarios multithread
- Combinez avec Weak pour éviter les cycles de références
Quelle est la différence clé entre RefCell et RwLock en Rust?
Considérez ces exemples :
use std::cell::RefCell;use std::sync::RwLock;
// Example 1let data = RefCell::new(vec![1, 2, 3]);let borrowed = data.borrow_mut();
// Example 2let shared = RwLock::new(vec![1, 2, 3]);let locked = shared.write().unwrap();RefCell et RwLock servent des buts similaires mais dans des contextes différents :
// Single-threaded scenario with RefCelluse std::cell::RefCell;
struct SingleThreaded { data: RefCell<Vec<i32>>,}
impl SingleThreaded { fn modify(&self) { self.data.borrow_mut().push(42); }}
// Multi-threaded scenario with RwLockuse std::sync::RwLock;
struct ThreadSafe { data: RwLock<Vec<i32>>,}
impl ThreadSafe { fn modify(&self) { self.data.write().unwrap().push(42); }}Différences clés :
- RefCell:
- uniquement monothread
- aucun surcoût de synchronisation
- panique en cas de violation d’emprunt
- RwLock:
- sûr pour les threads
- comporte un surcoût de synchronisation
- peut bloquer les threads au lieu de paniquer
Bonnes pratiques :
- Utilisez RefCell pour la mutabilité intérieure monothread
- Utilisez RwLock lorsque la sécurité des threads est requise
- Envisagez Mutex pour une mutabilité thread‑safe plus simple
- Documentez clairement les exigences de sécurité des threads
Que se passe-t-il lorsque ce code s’exécute ?
use std::sync::{Arc, Mutex};
fn main() { let lock = Arc::new(Mutex::new(42)); let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);}Ce code illustre un scénario d’impasse classique. Voici comment le corriger :
use std::sync::{Arc, Mutex};
// Correct way - Release lock before acquiring it againfn safe_mutex() { let lock = Arc::new(Mutex::new(42));
{ let mut data = lock.lock().unwrap(); *data += 1; } // Lock is released here
// Now we can acquire it again let data2 = lock.lock().unwrap(); println!("Value: {}", data2);}
// Using multiple mutexes safelyfn multiple_mutexes() { let lock1 = Arc::new(Mutex::new(42)); let lock2 = Arc::new(Mutex::new(43));
// Always acquire locks in the same order let guard1 = lock1.lock().unwrap(); let guard2 = lock2.lock().unwrap();}Bonnes pratiques pour éviter les impasses :
- Garder les sections critiques petites
- Libérer les verrous rapidement en utilisant des portées
- Acquérir plusieurs verrous dans un ordre cohérent
- Utiliser parking_lot::Mutex pour de meilleures performances
- Envisager d’utiliser RwLock pour les charges de travail à forte lecture
Modèles courants :
// Thread-safe counterstruct Counter { count: Arc<Mutex<i32>>,}
impl Counter { fn increment(&self) { let mut count = self.count.lock().unwrap(); *count += 1; } // Lock automatically released here}Que se passe-t-il lorsque vous exécutez ce code avec des références faibles ?
use std::rc::{Rc, Weak};
fn main() { let data = Rc::new(String::from("Wisdom")); let weak = Rc::downgrade(&data); // Create weak reference drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());}Les références faibles n’empêchent pas la désallocation de leurs cibles. Voici un exemple détaillé :
use std::rc::{Rc, Weak};use std::cell::RefCell;
// Parent-child tree structure avoiding reference cyclesstruct Node { next: Option<Rc<Node>>, parent: RefCell<Weak<Node>>, // Weak to prevent cycles value: i32,}
impl Node { fn new(value: i32) -> Rc<Node> { Rc::new(Node { next: None, parent: RefCell::new(Weak::new()), value, }) }
fn set_parent(&self, parent: &Rc<Node>) { *self.parent.borrow_mut() = Rc::downgrade(parent); }
fn get_parent(&self) -> Option<Rc<Node>> { self.parent.borrow().upgrade() }}Cas d’utilisation courants :
- Structures de type cache où les entrées peuvent être purgées
- Structures arborescentes avec références parentales
- Patrons d’observateur où les sujets peuvent être supprimés
- Rompre les cycles de références dans des structures de données complexes
Bonnes pratiques :
- Utiliser les références faibles pour les relations optionnelles
- Vérifier les résultats de upgrade() avant de les utiliser
- Documenter clairement les relations de propriété
- Envisager des alternatives comme des indices pour les cas plus simples
Que se passe-t-il avec le descripteur de fichier dans cet exemple RAII ?
use std::fs::File;
struct FileWrapper { file: File,}
fn main() { let file = File::create("test.txt").unwrap(); let wrapper = FileWrapper { file }; // ... use wrapper ... // No Drop implementation}RAII en Rust garantit que les ressources sont correctement gérées. Dans cet exemple, FileWrapper n’a pas besoin d’une implémentation personnalisée de Drop pour que le descripteur de fichier se ferme : son champ File est libéré automatiquement lorsque l’enveloppe sort de la portée.
Vous n’implémentez Drop que lorsque l’enveloppe elle‑même nécessite un nettoyage supplémentaire au‑delà de la libération de ses champs :
use std::fs::File;use std::io::{self, Write};
struct FileWrapper { file: File, path: String,}
impl FileWrapper { fn new(path: &str) -> io::Result<FileWrapper> { Ok(FileWrapper { file: File::create(path)?, path: path.to_string(), }) }
fn write(&mut self, content: &str) -> io::Result<()> { self.file.write_all(content.as_bytes()) }}
impl Drop for FileWrapper { fn drop(&mut self) { // Ensure file is properly closed // Could also do cleanup like deletion println!("Closing file: {}", self.path); }}Patrons RAII :
- Le constructeur acquiert les ressources
- Les méthodes utilisent les ressources en toute sécurité
- Les champs sont libérés automatiquement lorsque le propriétaire sort de la portée
- Un Drop personnalisé ajoute un nettoyage supplémentaire si besoin
- Utilisez
?pour la propagation d’erreurs
Bonnes pratiques :
- S’appuyer sur les implémentations Drop de la bibliothèque standard lorsqu’elles modélisent déjà la ressource
- Garder la gestion des ressources simple et évidente
- Utiliser les types de la bibliothèque standard quand c’est possible
- Documenter le comportement de nettoyage
- Envisager d’utiliser des motifs de garde pour les opérations à portée limitée
Que se passe-t-il lorsqu’on clone cette structure Philosophy ?
#[derive(Clone)]struct Philosophy { school: String, founder: String,}
fn main() { let stoicism = Philosophy { school: String::from("Stoicism"), founder: String::from("Zeno of Citium") }; let new_school = stoicism.clone(); println!("{} - {}", stoicism.school, new_school.school);}Comprenons en détail Copy vs Clone :
// Types that can be Copy#[derive(Copy, Clone)]struct Point { x: i32, y: i32,}
// Types that can only be Clone#[derive(Clone)]struct ComplexData { name: String, // String can't be Copy points: Vec<i32> // Vec can't be Copy}
// Manual implementation example#[derive(Debug)]struct Custom { data: Vec<i32>, identifier: u32,}
impl Clone for Custom { fn clone(&self) -> Self { Custom { data: self.data.clone(), identifier: self.identifier, // Copy type } }}Principales différences:
- Copy:
- Implicite, copie bit à bit
- Doit être sûr pour Copy (pas d’allocation sur le tas)
- Typiquement pour les petits types uniquement sur la pile
- Clone:
- Explicite, potentiellement une copie profonde
- Peut gérer les allocations sur le tas
- Plus flexible mais potentiellement coûteux
Bonnes pratiques :
- Implémentez Copy pour les petits types uniquement sur la pile
- Utilisez Clone pour les types avec des ressources possédées
- Documentez les implications de performance de Clone
- Envisagez des implémentations personnalisées de Clone pour l’optimisation
- Soyez prudent avec la dérivation automatique
Sur une cible Rust 64 bits typique actuelle, quelle est la taille de cette structure ?
struct Metadata { id: u32, // How many bytes? name: String, // How many bytes? active: bool // How many bytes + padding?}Décomposons la disposition mémoire de la struct et son optimisation :
// Typical current 64-bit Rust layout: 32 bytesstruct Metadata { id: u32, // 4 bytes name: String, // 24 bytes on 64-bit systems active: bool // 1 byte + padding/alignment}
// Reordering fields may reduce padding for repr(C) structs,// but default Rust layout is not a stable ABI guarantee.struct OptimizedMetadata { name: String, // 24 bytes id: u32, // 4 bytes active: bool // 1 byte + 3 padding}
// Further optimization with packing#[repr(packed)]struct PackedMetadata { id: u32, active: bool, name: String,}Considérations de disposition mémoire :
- Exigences d’alignement :
- u32 : alignement de 4 octets
- String : alignement de 8 octets et taille de 24 octets sur les cibles 64 bits courantes
- bool : alignement de 1 octet
- Stratégies d’ordre des champs :
- Regrouper les champs de taille similaire
- Placer les alignements les plus grands en premier
- Envisager l’optimisation des lignes de cache
Bonnes pratiques :
- Pour le FFI ou des hypothèses de mise en page stable, utilisez un
repr(...)approprié - Utilisez des tailles d’entiers appropriées
- Envisagez d’utiliser
Optionpour les champs optionnels - Mesurez les structs critiques en taille avec
std::mem::size_of - Utilisez
#[repr(packed)]avec prudence – cela peut affecter les performances
Comment les performances de ces deux implémentations se comparent-elles ?
// Implementation A: Iteratorfn sum_iterator(v: &[i32]) -> i32 { v.iter().fold(0, |acc, &x| acc + x)}
// Implementation B: Raw loopfn sum_loop(v: &[i32]) -> i32 { let mut sum = 0; for i in 0..v.len() { sum += v[i]; } sum}Les abstractions à coût nul de Rust se compilent en un code équivalent et efficace :
use std::ops::Range;
// High-level abstractiontrait ZeroCost { fn process(&self) -> u32;}
impl ZeroCost for Range<u32> { fn process(&self) -> u32 { self.fold(0, |acc, x| acc + x) }}
// Compiles to essentially the same code as:fn manual_process(range: Range<u32>) -> u32 { let mut sum = 0; let mut i = range.start; while i < range.end { sum += i; i += 1; } sum}
// Even more abstractions, still zero-costfn complex_processing<T>(data: &[T]) -> u32where T: AsRef<str> { data.iter() .map(|s| s.as_ref().len()) .filter(|&n| n > 3) .fold(0, |acc, n| acc + n as u32)}Principes clés :
- Ce que vous n’utilisez pas, vous ne le payez pas
- Ce que vous utilisez, vous ne pourriez pas le coder à la main mieux
Bonnes pratiques :
- Utilisez librement les abstractions de haut niveau
- Faites confiance aux optimisations du compilateur
- Profilez avant d’optimiser
- Priorisez la lisibilité d’abord
- Utilisez les itérateurs et les fermetures sans crainte
Merci d’avoir participé au quiz ! Si vous avez aimé tester vos connaissances en Rust, consultez mes autres défis de programmation! 🧠
Vous voulez pousser vos compétences Rust au niveau supérieur ? Voici quelques ressources recommandées :
- Livre Rust – Chapitre 4 : Propriété
- Rust par l’exemple – Gestion de la mémoire
- Référence Rust – Modèle de mémoire