From ed2e1cd102eb0b8b3f5aa179754b64b0bc1bd5cf Mon Sep 17 00:00:00 2001 From: rabot Date: Wed, 7 Oct 2026 12:37:15 +0200 Subject: [PATCH] Compte rendu --- COMPTE_RENDU.md | 290 +++++++++++++++++++++++++++++++++++++++++- out/mesures_bilan.csv | 11 ++ 2 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 out/mesures_bilan.csv diff --git a/COMPTE_RENDU.md b/COMPTE_RENDU.md index a48c3f8..3ad544a 100644 --- a/COMPTE_RENDU.md +++ b/COMPTE_RENDU.md @@ -1 +1,289 @@ -// Piste : réduire le temps d'exécution de `doit_echanger` +# Compte rendu — Profiling et optimisation de l'analyseur de mots + +**R5.A.04 — Qualité algorithmique** + +Les optimisations ont ramené le temps réel médian du programme de **2,81 s +à 0,11 s**, sur une comparaison de cinq exécutions par version sans +instrumentation. Le rapport produit et les statistiques affichées restent +identiques. L'empreinte du rapport est toujours **`1d53de903efd2b7c`**. + +## 1. Objectif et méthode de mesure + +Le programme génère un texte pseudo-aléatoire avec une graine fixe +(`20261006`), normalise ses lignes, compte les mots hors mots vides, les +classe et calcule des statistiques. Le jeu de données contient 250 000 +mots, répartis sur 21 746 lignes, dont 5 866 mots distincts retenus. + +L'objectif est de réduire le temps d'exécution en conservant les résultats. +Les mesures et les choix décrits ci-dessous proviennent de [notes.md](notes.md) +et des profils enregistrés dans `out/`. Une campagne complémentaire a été +réalisée pour mesurer le temps global et le pic mémoire des versions +initiale et finale dans les mêmes conditions. + +Deux compilations sont utilisées : + +- `gcc -O0` pour mesurer le temps réel d'exécution ; +- `gcc -O0 -pg` pour obtenir le profil plat et le graphe d'appels avec `gprof`. + +La première mesure consignée dans les notes donne **2,858 s sans `-pg`**, +contre **6,354 s avec `-pg`**. L'instrumentation a donc un coût important. +Les gains de performance sont calculés entre exécutables compilés sans +`-pg`, en conservant `-O0` pour observer l'effet des changements algorithmiques. +Les captures intermédiaires dont les options de compilation ne sont pas +précisées ne servent pas au calcul des gains. + +Les deux premières optimisations ont fait l'objet de mesures ponctuelles. +Pour les trois suivantes, les valeurs retenues sont les médianes de cinq +exécutions par version, en alternant les versions avant et après modification. + +## 2. Analyse du profil initial + +Le [profil initial](out/profil_avant.txt) fait apparaître les coûts suivants. +Le temps propre correspond au temps passé dans la fonction elle-même, +hors fonctions qu'elle appelle. + +| Fonction | Nombre d'appels | Part du temps propre | +| --- | ---: | ---: | +| `longueur` | 36 009 819 | 36,36 % | +| `egales` | 308 701 874 | 26,32 % | +| `chercher_mot` | 150 182 | 18,18 % | +| `trier_entrees` | 1 | 5,26 % | +| `longueur_moyenne` | 5 867 | 3,35 % | + +Le graphe d'appels permet d'expliquer ces résultats : + +- **Recherche des mots.** `chercher_mot` parcourt les entrées une à une. + Ses 150 182 recherches provoquent 296 656 203 appels à `egales`. + Le problème vient du nombre de comparaisons effectuées. +- **Statistiques.** `ecart_type_longueur` recalcule la même moyenne pour + chaque mot distinct. Avec l'appel effectué dans `main`, cela représente + 5 867 calculs de moyenne et 34 415 822 appels à `longueur` depuis + `longueur_moyenne`. +- **Parcours des chaînes.** `normaliser_ligne` recalcule la longueur de la + ligne à chaque caractère. `ajouter` recherche également la fin de la + destination à chaque ajout, notamment lorsque le rapport grandit. +- **Tri.** Le tri à bulles effectue systématiquement `n(n - 1)/2` + comparaisons, soit 17 202 045 appels à `doit_echanger` pour 5 866 entrées. + +Le nombre d'appels doit être interprété avec le travail effectué par appel : +parcourir un rapport déjà long coûte davantage que parcourir un mot court. +Les modifications sont donc évaluées par un nouveau chronométrage, en +complément du profil. + +## 3. Pistes d'amélioration, classées par gain attendu + +Dans les complexités indiquées, `n` désigne le nombre de mots distincts +et `L` la longueur d'une ligne. Les mots conservés sont limités à 31 caractères. + +| Priorité | Piste | Gain attendu et justification | +| ---: | --- | --- | +| 1 | Remplacer la recherche linéaire par une recherche dichotomique, puis étudier le hachage | Très élevé face à la version initiale : éviter des centaines de millions de comparaisons. | +| 2 | Calculer la moyenne une seule fois | Très élevé : supprimer un calcul identique répété pour chaque entrée et passer de O(n²) à O(n). | +| 3 | Éviter les recalculs de longueur des lignes et des chaînes construites | Élevé : supprimer les parcours répétés de chaînes longues. | +| 4 | Remplacer le tri à bulles par un tri en O(n log n) | Élevé une fois les autres coûts réduits : diminuer les 17 millions de comparaisons du tri. | +| 5 | Mémoriser la longueur des mots | Plus limité isolément : après la correction de la moyenne, les statistiques ne font plus que 11 732 calculs de longueur sur des mots courts. | + +La priorité porte ainsi sur la réduction du nombre de calculs. Les fonctions +`egales` et `doit_echanger` peuvent conserver leur comportement pendant que +les algorithmes qui les utilisent changent. + +## 4. Optimisations réalisées + +### 4.1. Recherche dichotomique + +Une première version maintient le tableau dans l'ordre alphabétique pendant +le comptage. La recherche élimine la moitié des candidats à chaque étape : +elle passe de O(n) à O(log n). L'insertion d'un nouveau mot reste en O(n), +car elle nécessite de décaler des entrées, mais les occurrences suivantes +n'entraînent aucun déplacement. + +Le nombre de comparaisons de chaînes dans la recherche passe de +296 656 203 à 1 718 447, soit environ **173 fois moins**. Cette étape est +conservée dans l'historique ; la version finale utilise le hachage. + +### 4.2. Calcul unique de la moyenne + +`main` calcule la moyenne une seule fois, puis la transmet à +`ecart_type_longueur`. La boucle de calcul de la variance réutilise cette +valeur. Le nombre d'appels à `longueur_moyenne` passe de 5 867 à **1**, +et le calcul des statistiques passe de O(n²) à O(n), sans changer la formule. + +### 4.3. Table de hachage pour le comptage + +Un index de 40 001 cases associe chaque mot à son entrée dans le tableau. +Lors d'une collision, le programme compare les mots puis examine la case +suivante. Les nouveaux mots sont ajoutés en fin de tableau, sans décalage. +La limite de 20 000 mots distincts maintient l'index rempli à moins de 50 %. + +La recherche et l'insertion deviennent O(1) en moyenne pour ces mots de +longueur bornée, avec un pire cas en O(n) en cas de nombreuses collisions. +L'index est libéré après le comptage, avant le tri qui déplace les entrées. + +La recherche effectue désormais **153 101 comparaisons**, environ 11,2 fois +moins qu'avec la dichotomie. Le gain global est cependant de 6,4 % à cette +étape : la recherche était déjà optimisée et les autres traitements +représentaient l'essentiel du temps restant. + +### 4.4. Longueurs mémorisées et parcours des chaînes + +Trois changements suppriment les appels restants à `longueur` : + +- le champ `taille` de `Entree` reçoit la longueur déjà calculée pendant + le découpage du mot ; la moyenne et la variance lisent directement ce champ ; +- `normaliser_ligne` parcourt la ligne jusqu'à `ligne[i] == '\0'`, ce qui + ramène sa complexité de O(L²) à O(L) ; +- `ajouter` reçoit une position de fin qui avance pendant la copie des + nouveaux caractères. Le rapport et les mots du vocabulaire sont ainsi + construits sans reparcourir leur préfixe à chaque ajout. + +Les **1 599 863 appels restants à `longueur` disparaissent** et la fonction +est supprimée. Le gain mesuré concerne cet ensemble de changements ; il ne +peut pas être attribué au seul stockage de la longueur des mots. + +### 4.5. Tri par tas + +Le tri à bulles est remplacé par un tri par tas. Le tas place à sa racine +l'entrée qui doit apparaître le plus tard dans le classement. Cette entrée +est placée à la fin de la partie non triée, puis le tas est rétabli. + +Le tri est en **O(n log n), y compris dans le pire cas**, avec O(1) de mémoire +supplémentaire. `doit_echanger` reste inchangée : fréquence décroissante, +puis ordre alphabétique croissant. Les échanges déplacent les entrées +entières, avec leur longueur mémorisée. + +Les appels à `doit_echanger` passent de **17 202 045 à 129 602**, soit environ +133 fois moins. Les comparaisons alphabétiques passent de 12 172 087 à 32 813. + +Ces cinq étapes correspondent aux commits `be387e2`, `d3f1987`, `57aaec4`, +`1c4405c` et `57c9ce9`. + +## 5. Temps mesurés et gains obtenus + +### Mesures par étape issues des notes + +Chaque gain est relatif à la version immédiatement précédente. Les mesures +ont été refaites à chaque étape, ce qui explique les petits écarts entre +le temps « après » d'une ligne et le temps « avant » de la suivante. +Les pourcentages sont ceux consignés dans les notes ; les temps affichés +sont arrondis. + +| Modification | Avant | Après | Réduction du temps | Profil obtenu | +| --- | ---: | ---: | ---: | --- | +| Recherche dichotomique | 2,80 s | 1,45 s | ≈ 48 % | [profil_apres.txt](out/profil_apres.txt) | +| Moyenne calculée une seule fois | 1,46 s | 0,88 s | ≈ 40 % | [profil_apres_moyenne.txt](out/profil_apres_moyenne.txt) | +| Table de hachage | 0,888 s | 0,831 s | 6,4 % | [profil_apres_hachage.txt](out/profil_apres_hachage.txt) | +| Longueurs et parcours des chaînes | 0,832 s | 0,362 s | 56,5 % | [profil_apres_longueurs.txt](out/profil_apres_longueurs.txt) | +| Tri par tas | 0,367 s | 0,113 s | 69,3 % | [profil_apres_tri.txt](out/profil_apres_tri.txt) | + +Le profil final est donc `out/profil_apres_tri.txt`. Le fichier +`out/profil_apres.txt` correspond uniquement à la première optimisation. + +### Comparaison globale complémentaire + +Pour compléter les notes, la version initiale du commit `55320bd` et la +version finale ont été compilées avec GCC 16.2.1 sous Linux x86_64, avec +`-O0` et sans `-pg`. Cinq exécutions par version ont été réalisées en +alternance. `/usr/bin/time` a mesuré les durées et le maximum de mémoire +résidente (RSS). Les résultats bruts sont conservés dans +[mesures_bilan.csv](out/mesures_bilan.csv). + +| Mesure médiane sur cinq exécutions | Version initiale | Version finale | +| --- | ---: | ---: | +| Temps réel | 2,81 s | 0,11 s | +| Temps utilisateur | 2,80 s | 0,11 s | +| Pic mémoire résident | 6 664 Kio | 6 668 Kio | + +Le rapport des temps donne une accélération d'environ **25,5**, soit une +réduction d'environ **96,1 %**. Ces valeurs restent des mesures sur ce jeu +de données et cette machine ; les temps de cette campagne sont relevés +au centième de seconde. + +## 6. Validation des résultats + +Après chaque étape, le rapport et la sortie console ont été comparés à +la version précédente, octet par octet. La campagne complémentaire compare +également directement les versions initiale et finale. Les résultats +suivants sont conservés : + +| Résultat | Valeur commune aux versions initiale et finale | +| --- | --- | +| Lignes analysées | 21 746 | +| Mots au total | 250 000 | +| Mots distincts retenus | 5 866 | +| Longueur moyenne | 6,712 | +| Écart-type | 1,873 | +| Bigramme le plus fréquent | `me`, 17 607 fois, souvent suivi de `n` | +| Mot le plus fréquent | `rane`, 287 occurrences | +| Empreinte du rapport | **`1d53de903efd2b7c`** | + +Les compilations avec `-Wall -Wextra -Wpedantic` n'ont produit aucun +avertissement. Les validations consignées dans les notes comprennent +également des exécutions avec et sans `-pg`, ainsi que sous +AddressSanitizer et UndefinedBehaviorSanitizer, sans erreur détectée. + +Des tests temporaires ont couvert les collisions de hachage, les doublons, +les limites de capacité, les mots tronqués à 31 caractères et les ajouts +jusqu'à la limite d'un tampon. Le tri a été comparé à `qsort` sur des +tableaux de 0 à 20 000 entrées, aléatoires, déjà triés ou inversés, avec +des fréquences égales. La conservation des longueurs après tri a aussi +été vérifiée. + +## 7. Consommation mémoire et limites + +L'empreinte du **rapport** et l'empreinte **mémoire** sont deux mesures +différentes. La première est conservée, mais deux optimisations augmentent +la mémoire allouée sur cette machine : + +| Structure ajoutée ou agrandie | Mémoire supplémentaire | +| --- | ---: | +| Index de hachage : 40 001 entiers de 4 octets | 160 004 octets, soit environ 156 Kio | +| Champ `taille` : 4 octets pour chacune des 20 000 entrées allouées | 80 000 octets, soit environ 78 Kio | +| Total supplémentaire pendant le comptage | 240 004 octets, soit environ 234 Kio | + +L'index est libéré avant le tri, qui ne nécessite pas de tableau auxiliaire. +Les pics RSS mesurés sont proches et fluctuent : de 6 504 à 6 672 Kio pour +la version initiale, et de 6 652 à 6 672 Kio pour la version finale. Le RSS +mesure la mémoire résidente de l'ensemble du processus, et non la somme +des tailles demandées à `malloc` et `calloc`. + +Cette proximité des pics ne permet donc pas d'affirmer que la consommation +mémoire est strictement identique. Au sens de la mémoire allouée, la +contrainte de conservation mentionnée dans le [README](README.md) n'est +pas respectée par le hachage et la mémorisation des longueurs : ces choix +constituent un compromis entre temps d'exécution et mémoire. + +## 8. Analyse du profil final et suite possible + +Dans le profil final, `est_mot_vide` effectue encore **12 045 671 appels à +`egales`**, sur 12 198 772 appels au total. La recherche linéaire dans la +liste des mots vides constitue donc une prochaine piste d'optimisation, +par exemple avec une structure de recherche adaptée. Cette piste n'a pas +été mise en œuvre dans la version présentée. + +Le profil final ne cumule que 0,11 s de temps échantillonné, avec une +granularité de 0,01 s. Ses pourcentages doivent être interprétés avec +prudence : une ligne à `0,00 s` ne signifie pas que la fonction ne coûte +rien. Les nombres d'appels et les chronométrages répétés complètent cette +lecture. + +## 9. Commandes de reproduction + +Depuis la racine du dépôt, les commandes suivantes créent des exécutables +dans un dossier séparé et produisent un nouveau profil final : + +```bash +mkdir -p out/validation +git show 55320bd:analyseur.c > out/validation/analyseur_initial.c +gcc -O0 out/validation/analyseur_initial.c -o out/validation/analyseur_initial +gcc -O0 analyseur.c -o out/validation/analyseur_final +gcc -O0 -pg analyseur.c -o out/validation/analyseur_pg +cd out/validation +/usr/bin/time -f 'real=%e s; RSS_max=%M Kio' ./analyseur_initial +/usr/bin/time -f 'real=%e s; RSS_max=%M Kio' ./analyseur_final +./analyseur_pg +gprof -b ./analyseur_pg gmon.out > profil_apres.txt +``` + +Pour comparer les performances, répéter les deux exécutions sans `-pg` +cinq fois chacune, en alternant leur ordre, puis retenir les médianes. +Chaque exécution doit afficher l'empreinte `1d53de903efd2b7c`. diff --git a/out/mesures_bilan.csv b/out/mesures_bilan.csv new file mode 100644 index 0000000..aa22897 --- /dev/null +++ b/out/mesures_bilan.csv @@ -0,0 +1,11 @@ +version;execution;temps_reel_s;temps_user_s;temps_systeme_s;pic_rss_kio +initial;1;2.83;2.82;0.00;6664 +final;1;0.11;0.11;0.00;6672 +final;2;0.11;0.11;0.00;6664 +initial;2;2.80;2.80;0.00;6672 +initial;3;2.81;2.80;0.00;6564 +final;3;0.11;0.11;0.00;6672 +final;4;0.11;0.10;0.00;6668 +initial;4;2.79;2.79;0.00;6672 +initial;5;2.82;2.81;0.00;6504 +final;5;0.11;0.11;0.00;6652