DanLevy.net

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 :

  1. Cloner la chaîne (crée une nouvelle copie) :
let greeting = philosopher.clone();
  1. Utiliser une référence (emprunte la valeur) :
let greeting = &philosopher;
  1. 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 :

  1. 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
  1. Cloner la valeur (créer une nouvelle copie) :
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. 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 ownership

Chaque 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 :

  1. 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 scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. 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 :

  1. Chaque paramètre reçoit son propre paramètre de durée de vie
  2. 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
  3. 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 lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn 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 :

  1. Box fournit un pointeur de taille fixe (généralement 8 octets sur les systèmes 64 bits)
  2. Les données réelles sont stockées sur le tas
  3. 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 :

  1. Création initiale avec Rc::new(): compteur = 1
  2. Premier clone pour marcus: compteur = 2
  3. 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 Weak pour éviter les cycles de références

Bonnes pratiques :

  • Utilisez Rc quand vous avez besoin d’une possession partagée
  • Envisagez Arc pour 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 parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

Modèles courants :

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

Bonnes pratiques:

  1. Utilisez des types possédés (String) lorsque vous devez stocker des données indéfiniment
  2. Utilisez des références lorsque la durée de vie du struct est clairement plus courte que les données
  3. Envisagez plusieurs paramètres de durée de vie lorsque les références peuvent avoir des durées différentes
  4. 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 annotation
fn longest<'a>(text1: &'a str, text2: &'a str) -> &'a str {
if text1.len() > text2.len() {
text1
} else {
text2
}
}
// Alternative with different lifetimes
fn 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 :

  1. Plusieurs références d’entrée peuvent avoir des durées de vie différentes
  2. La valeur de retour doit vivre aussi longtemps que les deux entrées
  3. Le compilateur doit vérifier ces relations

Modèles courants :

// Single input reference - elision works
fn first_word(s: &str) -> &str { /* ... */ }
// Multiple references, same lifetime needed
fn compare_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { /* ... */ }
// Different lifetimes possible
fn combine<'a, 'b>(s1: &'a str, s2: &'b str) -> String { /* ... */ }

Bonnes pratiques :

  1. Laisser l’élision des durées de vie fonctionner quand c’est possible
  2. Utiliser des durées de vie explicites lorsque les relations doivent être claires
  3. Envisager de retourner des types possédés pour éviter la complexité des durées de vie
  4. 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 :

  1. RefCell déplace les vérifications d’emprunt à l’exécution
  2. Peut provoquer des panics si les règles sont violées
  3. 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 :

  1. Privilégier l’emprunt à la compilation quand c’est possible
  2. Garder les emprunts RefCell dans des portées étroites
  3. Envisager d’utiliser drop() pour terminer explicitement les emprunts
  4. 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 types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

Différences clés :

  1. Cell :
  • Fonctionne au mieux avec les types Copy
  • Pas d’API d’emprunt
  • Copie ou déplace toujours les valeurs
  1. RefCell :
  • Fonctionne avec n’importe quel type
  • Possède une API d’emprunt
  • Vérification d’emprunt à l’exécution

Bonnes pratiques :

  1. Utilisez Cell pour les types Copy simples (nombres, bool, etc.)
  2. Utilisez RefCell quand vous devez emprunter le contenu
  3. Gardez les mutations via Cell/RefCell au minimum
  4. 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 structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

Points clés:

  1. 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
  1. 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
  1. 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 :

  1. Privilégiez la propriété unique quand c’est possible
  2. Utilisez Rc pour un partage monothread
  3. Utilisez Arc pour les scénarios multithread
  4. 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 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let 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 RefCell
use std::cell::RefCell;
struct SingleThreaded {
data: RefCell<Vec<i32>>,
}
impl SingleThreaded {
fn modify(&self) {
self.data.borrow_mut().push(42);
}
}
// Multi-threaded scenario with RwLock
use std::sync::RwLock;
struct ThreadSafe {
data: RwLock<Vec<i32>>,
}
impl ThreadSafe {
fn modify(&self) {
self.data.write().unwrap().push(42);
}
}

Différences clés :

  1. RefCell:
  • uniquement monothread
  • aucun surcoût de synchronisation
  • panique en cas de violation d’emprunt
  1. RwLock:
  • sûr pour les threads
  • comporte un surcoût de synchronisation
  • peut bloquer les threads au lieu de paniquer

Bonnes pratiques :

  1. Utilisez RefCell pour la mutabilité intérieure monothread
  2. Utilisez RwLock lorsque la sécurité des threads est requise
  3. Envisagez Mutex pour une mutabilité thread‑safe plus simple
  4. 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 again
fn 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 safely
fn 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 :

  1. Garder les sections critiques petites
  2. Libérer les verrous rapidement en utilisant des portées
  3. Acquérir plusieurs verrous dans un ordre cohérent
  4. Utiliser parking_lot::Mutex pour de meilleures performances
  5. Envisager d’utiliser RwLock pour les charges de travail à forte lecture

Modèles courants :

// Thread-safe counter
struct 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 cycles
struct 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 :

  1. Structures de type cache où les entrées peuvent être purgées
  2. Structures arborescentes avec références parentales
  3. Patrons d’observateur où les sujets peuvent être supprimés
  4. Rompre les cycles de références dans des structures de données complexes

Bonnes pratiques :

  1. Utiliser les références faibles pour les relations optionnelles
  2. Vérifier les résultats de upgrade() avant de les utiliser
  3. Documenter clairement les relations de propriété
  4. 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 :

  1. Le constructeur acquiert les ressources
  2. Les méthodes utilisent les ressources en toute sécurité
  3. Les champs sont libérés automatiquement lorsque le propriétaire sort de la portée
  4. Un Drop personnalisé ajoute un nettoyage supplémentaire si besoin
  5. Utilisez ? pour la propagation d’erreurs

Bonnes pratiques :

  1. S’appuyer sur les implémentations Drop de la bibliothèque standard lorsqu’elles modélisent déjà la ressource
  2. Garder la gestion des ressources simple et évidente
  3. Utiliser les types de la bibliothèque standard quand c’est possible
  4. Documenter le comportement de nettoyage
  5. 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:

  1. 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
  1. Clone:
  • Explicite, potentiellement une copie profonde
  • Peut gérer les allocations sur le tas
  • Plus flexible mais potentiellement coûteux

Bonnes pratiques :

  1. Implémentez Copy pour les petits types uniquement sur la pile
  2. Utilisez Clone pour les types avec des ressources possédées
  3. Documentez les implications de performance de Clone
  4. Envisagez des implémentations personnalisées de Clone pour l’optimisation
  5. 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 bytes
struct 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 :

  1. 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
  1. 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 :

  1. Pour le FFI ou des hypothèses de mise en page stable, utilisez un repr(...) approprié
  2. Utilisez des tailles d’entiers appropriées
  3. Envisagez d’utiliser Option pour les champs optionnels
  4. Mesurez les structs critiques en taille avec std::mem::size_of
  5. Utilisez #[repr(packed)] avec prudence – cela peut affecter les performances

Comment les performances de ces deux implémentations se comparent-elles ?

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn 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 abstraction
trait 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-cost
fn complex_processing<T>(data: &[T]) -> u32
where 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 :

  1. Ce que vous n’utilisez pas, vous ne le payez pas
  2. Ce que vous utilisez, vous ne pourriez pas le coder à la main mieux

Bonnes pratiques :

  1. Utilisez librement les abstractions de haut niveau
  2. Faites confiance aux optimisations du compilateur
  3. Profilez avant d’optimiser
  4. Priorisez la lisibilité d’abord
  5. 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 :