Exercice 1 : Pointeurs et allocation dynamique en C
🌐 Énoncé du problème / Étude de cas
Section titled “🌐 Énoncé du problème / Étude de cas”Dans le cadre de la conception d’un système embarqué à ressources contraintes (par exemple, un microcontrôleur destiné à l’acquisition de données sismiques ou environnementales), on s’intéresse à la représentation et à la manipulation de séries temporelles de grande taille.
Les données proviennent de capteurs haute fréquence générant des flux continus. Pour analyser ces flux sans saturer la mémoire vive (RAM) disponible — typiquement sur une cible de type ARM Cortex-M —, on modélise le signal sous la forme d’une liste chaînée dynamique de blocs contigus, appelés chunks.
L’objectif de ce problème est de concevoir, implémenter et analyser rigoureusement en langage C un module de gestion de mémoire pour ces structures, en évitant les fuites mémoire, les dépassements de tampons (buffer overflows) et en optimisant le coût d’allocation dynamique.
On pose les structures de données fondamentales suivantes en C :
#include <stdint.h>#include <stdlib.h>
#define CHUNK_SIZE 256 // Taille fixe d'un bloc de données en nombre d'échantillons
typedef struct DataChunk { int32_t values[CHUNK_SIZE]; // Données brutes (entiers signés 32 bits) size_t count; // Nombre d'éléments réellement stockés (0 <= count <= CHUNK_SIZE) struct DataChunk* next; // Pointeur vers le bloc suivant} DataChunk;
typedef struct { DataChunk* head; // Premier bloc de la liste DataChunk* tail; // Dernier bloc pour l'insertion en O(1) size_t total_elements; // Nombre total d'échantillons stockés} SignalStream;❓ Questions progressives
Section titled “❓ Questions progressives”I. Analyse théorique et modélisation mémoire
Section titled “I. Analyse théorique et modélisation mémoire”- [Q1] Calculer l’empreinte mémoire exacte (en octets) d’une structure
DataChunksur une architecture 64-bit standard (où un pointeur occupe octets, unsize_toctets, et unint32_toctets). Justifier la présence éventuelle de bourrage mémoire (padding). - [Q2] Si l’on souhaite stocker un signal de échantillons, estimer la mémoire totale occupée par les structures
DataChunk(données utiles + métadonnées de chaînage). En déduire le taux de surcoût (overhead) mémoire induit par l’utilisation de cette liste chaînée par rapport à un tableau contigus géant. - [Q3] Analyse critique : Discuter des limites de ce modèle pour un système temps réel dur. Quels sont les risques liés aux appels répétés à la fonction
mallocdans une boucle d’acquisition haute fréquence ?
II. Implémentation et gestion sécurisée des pointeurs
Section titled “II. Implémentation et gestion sécurisée des pointeurs”- [Q4] Écrire une fonction
SignalStream* create_stream(void)qui initialise un flux de données vide de manière sécurisée (vérification des retours d’allocation). - [Q5] Écrire une fonction
int append_sample(SignalStream* stream, int32_t sample)qui ajoute un échantillon au flux. Si le dernier bloc est plein, un nouveau bloc doit être alloué dynamiquement et rattaché au flux. La fonction retournera1en cas de succès et0si l’allocation échoue. - [Q6] Proposer une analyse de la complexité temporelle dans le pire des cas et en moyenne pour l’insertion de échantillons dans un flux initialement vide via la fonction
append_sample.
III. Robustesse, récursivité et libération de la mémoire
Section titled “III. Robustesse, récursivité et libération de la mémoire”- [Q7] Écrire une fonction itérative
void free_stream_iterative(SignalStream* stream)qui libère proprement toute la mémoire associée au flux. - [Q8] Écrire une fonction récursive
void free_chunks_recursive(DataChunk* chunk)permettant de libérer les blocs de la liste chaînée. - [Q9] Analyse critique : Soit le nombre total de blocs
DataChunkdans la liste. Quelle est la complexité spatiale sur la pile d’exécution (call stack) de la version récursive de la question Q8 ? Pour un système embarqué disposant d’une pile de , quel est le nombre maximal de blocs tolérable avant un débordement de pile (stack overflow) ? Conclure sur l’adéquation de la récursivité dans ce contexte.
💡 Pistes & Indications
Section titled “💡 Pistes & Indications”- Q1 : Attention à l’alignement mémoire (alignment requirements) des types sur 64-bit (les pointeurs et
size_ts’alignent généralement sur 8 octets). - Q5 : Penser à maintenir à jour les champs
head,tail,countettotal_elements. Le cas où le flux est initialement vide (head == NULL) doit être traité comme un cas particulier. - Q8 & Q9 : Pour libérer récursivement une structure chaînée, il est impératif de libérer le reste de la liste avant de libérer le nœud courant, ou de transmettre le témoin correctement, tout en évaluant la profondeur de récursion.
📋 Corrigé détaillé et analyse des résultats
Section titled “📋 Corrigé détaillé et analyse des résultats”I. Analyse théorique et modélisation mémoire
Section titled “I. Analyse théorique et modélisation mémoire”-
[Corrigé Q1]
- Tableau
values: octets. - Champ
count: de typesize_t( octets). - Champ
next: pointeur versstruct DataChunk( octets). - Alignement et Padding : Sur une architecture 64-bit, la taille totale est souvent un multiple de l’alignement maximal requis (ici octets).
values: octets (divisible par ).count: octets.next: octets.- Total = octets. Aucun bourrage supplémentaire n’est strictement nécessaire car la somme est un multiple de . La taille d’un
DataChunkest donc de octets.
- Tableau
-
[Corrigé Q2]
- Nombre de blocs nécessaires pour échantillons avec
CHUNK_SIZE = 256: - Mémoire totale occupée : .
- Mémoire strictement utile : .
- Surcoût (overhead) : , soit un taux de surcoût d’environ . Ce modèle est donc très économe en mémoire par rapport aux métadonnées.
- Nombre de blocs nécessaires pour échantillons avec
-
[Corrigé Q3]
- Analyse critique : L’appel fréquent à
mallocdans une boucle d’acquisition temps réel pose deux problèmes majeurs :- Déterminisme temporel : L’allocateur de la bibliothèque standard (
malloc/free) n’a pas un temps d’exécution garanti en (risques de fragmentation du monceau ou heap fragmentation, recherche dans les listes de blocs libres). - Risque de saturation (Out Of Memory) : Si le système tourne indéfiniment sans politique de purge, l’allocation dynamique finira par échouer, entraînant un crash du programme.
- Déterminisme temporel : L’allocateur de la bibliothèque standard (
- Alternative industrielle : Utiliser un pool de mémoire pré-alloué (memory pool ou arena allocator) pour s’affranchir des appels dynamiques répétés.
- Analyse critique : L’appel fréquent à
II. Implémentation et gestion sécurisée des pointeurs
Section titled “II. Implémentation et gestion sécurisée des pointeurs”- [Corrigé Q4]
SignalStream* create_stream(void) { SignalStream* stream = (SignalStream*)malloc(sizeof(SignalStream)); if (stream == NULL) { return NULL; // Échec d'allocation } stream->head = NULL; stream->tail = NULL; stream->total_elements = 0; return stream;}- [Corrigé Q5]
int append_sample(SignalStream* stream, int32_t sample) { if (stream == NULL) return 0;
// Cas 1: Le flux est vide ou le dernier bloc est plein if (stream->tail == NULL || stream->tail->count == CHUNK_SIZE) { DataChunk* new_chunk = (DataChunk*)malloc(sizeof(DataChunk)); if (new_chunk == NULL) return 0; // Échec mémoire
new_chunk->count = 0; new_chunk->next = NULL;
if (stream->head == NULL) { stream->head = new_chunk; } else { stream->tail->next = new_chunk; } stream->tail = new_chunk; }
// Ajout effectif dans le bloc courant (tail) stream->tail->values[stream->tail->count] = sample; stream->tail->count++; stream->total_elements++;
return 1;}- [Corrigé Q6]
- Pour insertions :
- La grande majorité des insertions se fait dans le bloc existant en temps strict.
- Toutes les insertions, un appel à
mallocest effectué, ce qui prend un temps en en moyenne amortie (mais potentiellement plus long dans le pire des cas).
- En moyenne, le coût par insertion est en amorti. Dans le pire des cas (lorsqu’un
mallocest déclenché), une opération prend un temps linéaire par rapport au travail de l’allocateur, mais amorti sur opérations, le coût unitaire reste constant.
- Pour insertions :
III. Robustesse, récursivité et libération de la mémoire
Section titled “III. Robustesse, récursivité et libération de la mémoire”- [Corrigé Q7]
void free_stream_iterative(SignalStream* stream) { if (stream == NULL) return;
DataChunk* current = stream->head; while (current != NULL) { DataChunk* next_chunk = current->next; free(current); current = next_chunk; } free(stream);}- [Corrigé Q8]
void free_chunks_recursive(DataChunk* chunk) { if (chunk == NULL) return; free_chunks_recursive(chunk->next); // Appel récursif sur la suite free(chunk); // Libération post-ordre}
void free_stream_recursive(SignalStream* stream) { if (stream == NULL) return; free_chunks_recursive(stream->head); free(stream);}- [Corrigé Q9]
- Complexité spatiale : La fonction récursive
free_chunks_recursiveempile un cadre d’appel (stack frame) par bloc. La complexité en espace de la pile est donc en , où est le nombre de blocs. - Calcul critique de dimensionnement :
- Supposons qu’un cadre d’appel pour
free_chunks_recursiveconsomme environ octets sur la pile (sauvegarde des registres, adresse de retour, argumentchunk). - Avec une pile de octets, et en supposant l’absence d’autres fonctions actives, le nombre maximal de cadres d’appel est :
- Pour échantillons, blocs. , ce qui conduirait inévitablement à un stack overflow fatal.
- Supposons qu’un cadre d’appel pour
- Conclusion : Bien que élégante, la récursivité non terminale (tail-recursion) est fortement proscrite pour la manipulation de structures de données dynamiques profondes sur les systèmes embarqués à pile limitée. La version itérative (Q7) est la seule viable en production.
- Complexité spatiale : La fonction récursive